├── .github └── workflows │ └── tests.yml ├── .gitignore ├── Documentation ├── README.md ├── manopen_URL_Scheme.md └── x-man-page_URL_Scheme.md ├── LICENSE ├── ManOpen.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── ManOpen.xcscheme │ ├── cat2html.xcscheme │ ├── cat2rtf.xcscheme │ └── openman.xcscheme ├── ManOpen ├── AproposDocument.h ├── AproposDocument.m ├── DisplayPathFormatter.h ├── DisplayPathFormatter.m ├── FileURLComponents.h ├── FileURLComponents.m ├── Info-ManOpen.plist ├── LinkCursor.tiff ├── MVAppInfo.h ├── MVAppInfo.m ├── ManDocument.h ├── ManDocument.m ├── ManDocumentController.h ├── ManDocumentController.m ├── ManOpen.icns ├── ManOpen.scriptSuite ├── ManOpen.scriptTerminology ├── ManOpen.tiff ├── ManOpenURLComponents.h ├── ManOpenURLComponents.m ├── ManOpenURLHandlerCommand.h ├── ManOpenURLHandlerCommand.m ├── ManOpen_main.m ├── ManPage.h ├── ManPage.m ├── NSData+Utils.h ├── NSData+Utils.m ├── NSDictionary+ManOpen.h ├── NSDictionary+ManOpen.m ├── NSObject+PoofDragDataSource.h ├── NSString+ManOpen.h ├── NSString+ManOpen.m ├── NSURL+ManOpen.h ├── NSURL+ManOpen.m ├── NSUserDefaults+ManOpen.h ├── NSUserDefaults+ManOpen.m ├── PoofDragTableView.h ├── PoofDragTableView.m ├── PrefPanelController.h ├── PrefPanelController.m ├── SystemType.h ├── XManPageURLComponents.h ├── XManPageURLComponents.m ├── en.lproj │ ├── Apropos.xib │ ├── Credits.rtf │ ├── DocController.xib │ ├── InfoPlist.strings │ ├── ManOpen.xib │ ├── ManPage.xib │ └── PrefPanel.xib └── helpQMark.tiff ├── ManOpenTests ├── FileURLComponentsTests.m ├── Info.plist ├── ManDocumentControllerTests.m ├── ManOpenURLComponentsTests.m ├── ManOpenURLHandlerCommandTests.m ├── ManPageTests.m ├── MockManDocumentController.h ├── MockManDocumentController.m ├── NSDictionary+ManOpenTests.m ├── NSString+ManOpenTests.m ├── NSURL+ManOpenTests.m ├── NSUserDefaults+ManOpenPreferencesTests.m └── XManPageURLComponentsTests.m ├── README.md ├── TODO.md ├── cat2html └── cat2html.l ├── cat2rtf └── cat2rtf.l ├── openman ├── Application.h ├── Application.m ├── ApplicationTests.m ├── LaunchServices.h ├── LaunchServices.m ├── LaunchServicesFake.h ├── LaunchServicesFake.m ├── LaunchServicesTests.m ├── Version.h ├── Version.m ├── VersionTests.m ├── openman.1 ├── openman.m └── openmanTests-Info.plist └── scripts ├── GetURLForFile.scpt ├── GetURLForManopen.scpt ├── GetURLForManopenApropos.scpt ├── GetURLForManopenFilePath.scpt ├── GetURLForXManPage.scpt ├── GetURLForXManPageApropos.scpt └── OpenFile.scpt /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | Coverage: 5 | name: Coverage 6 | runs-on: macos-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Build 10 | run: | 11 | xcodebuild \ 12 | -project ManOpen.xcodeproj \ 13 | -scheme ManOpen \ 14 | -configuration Development \ 15 | -quiet \ 16 | test 17 | - name: Coverage 18 | run: | 19 | bash <(curl -s https://codecov.io/bash) 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | project.xcworkspace 3 | tmp 4 | xcuserdata 5 | 6 | -------------------------------------------------------------------------------- /Documentation/README.md: -------------------------------------------------------------------------------- 1 | # ManOpen Documentation 2 | 3 | - [The `manopen:` URL Scheme][1] 4 | - [The `x-man-page:` URL Scheme][2] 5 | 6 | [1]: ./manopen_URL_Scheme.md 7 | [2]: ./x-man-page_URL_Scheme.md 8 | 9 | -------------------------------------------------------------------------------- /Documentation/manopen_URL_Scheme.md: -------------------------------------------------------------------------------- 1 | # The `manopen:` URL Scheme 2 | 3 | In _ManOpen_ [version 2.6][1] and earlier, the `openman` command line tool uses 4 | [_Distributed Objects_][2] to launch and communicate with `ManOpen.app`, 5 | specifically the [`NSConnection`][3] and [`NSDistantObject`][4] classes. 6 | Unfortunately, _Distributed Objects_ is now long deprecated, so a replacement 7 | communication method is needed going forward. 8 | 9 | [1]: http://clindberg.org/projects/ManOpen.html 10 | [2]: https://web.archive.org/web/20090418184917/http://developer.apple.com/documentation/Cocoa/Conceptual/DistrObjects/DistrObjects.html 11 | [3]: https://developer.apple.com/documentation/foundation/nsconnection 12 | [4]: https://developer.apple.com/documentation/foundation/nsdistantobject 13 | 14 | The `openman` tool operates in three modes, with the following required and 15 | optional parameters being sent to `ManOpen.app`: 16 | 17 | 1. lookup _man_ page by `name`, with optional `section`, `MANPATH` and 18 | `background` flag 19 | 1. _apropos_ search by `keyword`, with optional `MANPATH` and `background` flag 20 | 1. open _man_ file by `path`, with optional `background` flag 21 | 22 | The `MANPATH` parameter can be used to modify the _man_ page search path. The 23 | `background` parameter can be used to display the requested _ManOpen_ document 24 | without forcing its window into the foreground. 25 | 26 | The [_Launch Services_][5] APIs in the _Core Services_ framework provides a 27 | modern programmatic way to open `ManOpen.app` and display a document. _Launch 28 | Services_ is built around URLs. 29 | 30 | [5]: https://developer.apple.com/documentation/coreservices/launch_services?language=objc 31 | 32 | The `manopen:` scheme is designed for launching _ManOpen_ in the three modes of 33 | the `openman` tool. It is similar to the [`x-man-page:`][6] scheme. 34 | 35 | [6]: ./x-man-page_URL_Scheme.md 36 | 37 | ## _man_ Page Lookup 38 | 39 | The _man_ page lookup URL has one of the formats: 40 | 41 | manopen://
/?MANPATH=&background=[true|false] 42 | manopen:///?MANPATH=&background=[true|false] 43 | 44 | where `
` is the _optional_ manual section number and `` is the 45 | _man_ page name to look up. The query parameters `MANPATH` and `background` are 46 | optional. Unlike the `x-man-page:` scheme, exactly two slashes are required 47 | before the `
`, whether present or not, and exactly one slash is 48 | required before the ``. 49 | 50 | ## _apropos_ Keyword Search 51 | 52 | The _apropos_ keyword search URL has the format: 53 | 54 | manopen://apropos/?MANPATH=&background=[true|false] 55 | 56 | where `` is the keyword to search for. The query parameters `MANPATH` 57 | and `background` are optional. Exactly two slashes are required before 58 | `apropos` and exactly one slash is required before the ``. 59 | 60 | ## Open _man_ File by Path 61 | 62 | The _man_ file URL has the format: 63 | 64 | manopen:?background=[true|false] 65 | 66 | where `` is the absolute path of the _man_ page file to open. 67 | The query parameter `background` is optional. The absolute file path must begin 68 | with exactly one slash. 69 | 70 | ## Query Parameters 71 | 72 | Use the `MANPATH` parameter to restrict or expand the _man_ page or _apropos_ 73 | search to a particular path. The value of `MANPATH` should be one or more 74 | absolute paths; join multiple paths together with the colon (`:`) character. 75 | The value of `MANPATH` should be URL encoded if it contains the ampersand (`&`) 76 | character. If not specified, _ManOpen_ uses the search path given in its 77 | preferences. 78 | 79 | The `background` parameter must have the value `true` or `false`. If not 80 | specified, _ManOpen_ will open a new window in the foreground. 81 | 82 | ## Examples 83 | 84 | manopen:///grep 85 | manopen://3/printf?background=true 86 | manopen://apropos/edit?MANPATH=/usr/man:/usr/local/man 87 | manopen:/usr/local/share/man/man1/wget.1 88 | 89 | -------------------------------------------------------------------------------- /Documentation/x-man-page_URL_Scheme.md: -------------------------------------------------------------------------------- 1 | # The `x-man-page:` URL Scheme 2 | 3 | The `x-man-page:` scheme has been supported by `Terminal.app` since at least 4 | Mac OS X 10.3 Panther, though there doesn't appear to be any official 5 | documentation for the scheme. There are two versions of `x-man-page:` URLs: 6 | _man_ page lookup and _apropos_ keyword search. _ManOpen_ supports the 7 | `x-man-page:` scheme. The [`ManOpenURLHandlerCommand`][1] class implements the 8 | `x-man-page:` support. 9 | 10 | [1]: ./../ManOpen/ManOpenURLHandlerCommand.m 11 | 12 | ## _man_ Page Lookup 13 | 14 | The _man_ page lookup URL has the format: 15 | 16 | x-man-page://
/ 17 | 18 | where `
` is the _optional_ manual section number and `` is the 19 | _man_ page name to look up. Examples of _man_ page lookup, using the `open` 20 | command in a terminal window: 21 | 22 | open "x-man-page://1/printf" 23 | open "x-man-page:///grep" 24 | 25 | The `Terminal.app` allows a variable number of slashes before the `` 26 | portion of the URL, but requires exactly two slashes before the `
`. 27 | All these variations are accepted: 28 | 29 | # with section and name 30 | open "x-man-page://1/printf" 31 | open "x-man-page://1//printf" 32 | open "x-man-page://1///printf" 33 | 34 | # with name only 35 | open "x-man-page:/grep" 36 | open "x-man-page://grep" 37 | open "x-man-page:///grep" 38 | open "x-man-page:////grep" 39 | 40 | ## _apropos_ Keyword Search 41 | 42 | The _apropos_ keyword search URL has the format: 43 | 44 | x-man-page:///;type=a 45 | 46 | where `` is the keyword to search for. Examples of _apropos_ keyword 47 | search, using the `open` command in a terminal window: 48 | 49 | open "x-man-page:///print;type=a" 50 | open "x-man-page:///regex;type=a" 51 | 52 | The `Terminal.app` allows one or more slashes before the `` part of 53 | the URL. All of these variations are accepted: 54 | 55 | open "x-man-page:///regex;type=a" 56 | open "x-man-page:/regex;type=a" 57 | 58 | A `
` can be given for _apropos_ search URLs, but is ignored. 59 | 60 | # section "1" is ignored 61 | open "x-man-page://1/print;type=a" 62 | 63 | ## `x-man-page:` Scheme References 64 | 65 | Sources of information on the `x-man-page` scheme: 66 | 67 | - [_x-man-page: URL handler studied for the OSX Terminal.app_][2]. 68 | - [_On Viewing `man` Pages_][3] 69 | - [_Shell Tricks: man pages_][4] 70 | - [_10.3: Use the x-man-page URL type to open UNIX man pages_][5] 71 | 72 | [2]: https://github.com/ouspg/urlhandlers/blob/master/cases/x-man-page.md 73 | [3]: http://scriptingosx.com/2017/04/on-viewing-man-pages/ 74 | [4]: http://brettterpstra.com/2014/08/05/shell-tricks-man-pages/ 75 | [5]: http://hints.macworld.com/article.php?story=20031225072602242 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2000-2012 Carl Lindberg 2 | Copyright (c) 2017 Don McCaughey 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /ManOpen.xcodeproj/xcshareddata/xcschemes/ManOpen.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 53 | 59 | 60 | 61 | 62 | 63 | 73 | 75 | 81 | 82 | 83 | 84 | 90 | 92 | 98 | 99 | 100 | 101 | 103 | 104 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /ManOpen.xcodeproj/xcshareddata/xcschemes/cat2html.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ManOpen.xcodeproj/xcshareddata/xcschemes/cat2rtf.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ManOpen.xcodeproj/xcshareddata/xcschemes/openman.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /ManOpen/AproposDocument.h: -------------------------------------------------------------------------------- 1 | /* AproposDocument.h created by lindberg on Tue 10-Oct-2000 */ 2 | 3 | #import "SystemType.h" 4 | #import 5 | 6 | @class NSMutableArray; 7 | @class NSTableColumn, NSTableView; 8 | 9 | @interface AproposDocument : NSDocument 10 | { 11 | NSString *title; 12 | NSString *searchString; 13 | NSMutableArray *titles; 14 | NSMutableArray *descriptions; 15 | 16 | IBOutlet NSTableView *tableView; 17 | IBOutlet NSTableColumn *titleColumn; 18 | } 19 | 20 | - (id)initWithString:(NSString *)apropos 21 | manPath:(NSString *)manPath 22 | title:(NSString *)title; 23 | 24 | - (void)parseOutput:(NSString *)output; 25 | 26 | - (IBAction)saveCurrentWindowSize:(id)sender; 27 | - (IBAction)openManPages:(id)sender; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /ManOpen/AproposDocument.m: -------------------------------------------------------------------------------- 1 | /* AproposDocument.m created by lindberg on Tue 10-Oct-2000 */ 2 | 3 | #import "AproposDocument.h" 4 | #import 5 | #import "ManDocumentController.h" 6 | #import "NSString+ManOpen.h" 7 | #import "NSUserDefaults+ManOpen.h" 8 | #import "PrefPanelController.h" 9 | 10 | @interface NSDocument (LionRestorationMethods) 11 | - (void)encodeRestorableStateWithCoder:(NSCoder *)coder; 12 | - (void)restoreStateWithCoder:(NSCoder *)coder; 13 | @end 14 | 15 | @implementation AproposDocument 16 | 17 | + (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName 18 | { 19 | return YES; 20 | } 21 | 22 | - (void)_loadWithString:(NSString *)apropos manPath:(NSString *)manPath title:(NSString *)aTitle 23 | { 24 | ManDocumentController *docController = [ManDocumentController sharedDocumentController]; 25 | NSMutableString *command = [docController manCommandWithManPath:manPath]; 26 | NSData *output; 27 | 28 | titles = [[NSMutableArray alloc] init]; 29 | descriptions = [[NSMutableArray alloc] init]; 30 | title = [aTitle retain]; 31 | [self setFileType:@"apropos"]; 32 | 33 | /* Searching for a blank string doesn't work anymore... use a catchall regex */ 34 | if ([apropos length] == 0) 35 | apropos = @"."; 36 | searchString = [apropos retain]; 37 | 38 | /* 39 | * Starting on Tiger, man -k doesn't quite work the same as apropos directly. 40 | * Use apropos then, even on Panther. Panther/Tiger no longer accept the -M 41 | * argument, so don't try... we set the MANPATH environment variable, which 42 | * gives a warning on Panther (stderr; ignored) but not on Tiger. 43 | */ 44 | // [command appendString:@" -k"]; 45 | [command setString:@"/usr/bin/apropos"]; 46 | 47 | [command appendFormat:@" %@", apropos.singleQuotedShellWord]; 48 | output = [docController dataByExecutingCommand:command manPath:manPath]; 49 | /* The whatis database appears to not be UTF8 -- at least, UTF8 can fail, even on 10.7 */ 50 | [self parseOutput:[[[NSString alloc] initWithData:output encoding:NSMacOSRomanStringEncoding] autorelease]]; 51 | } 52 | 53 | - (id)initWithString:(NSString *)apropos 54 | manPath:(NSString *)manPath 55 | title:(NSString *)aTitle 56 | { 57 | self = [super init]; 58 | if (self) { 59 | [self _loadWithString:apropos 60 | manPath:manPath 61 | title:aTitle]; 62 | 63 | if ([titles count] == 0) { 64 | NSAlert *alert = [[NSAlert new] autorelease]; 65 | alert.messageText = @"Nothing found"; 66 | alert.informativeText = [NSString stringWithFormat:@"No pages related to '%@' found", apropos]; 67 | [alert runModal]; 68 | [self release]; 69 | return nil; 70 | } 71 | } 72 | return self; 73 | } 74 | 75 | - (void)dealloc 76 | { 77 | [title release]; 78 | [titles release]; 79 | [descriptions release]; 80 | [searchString release]; 81 | [super dealloc]; 82 | } 83 | 84 | - (NSString *)windowNibName 85 | { 86 | return @"Apropos"; 87 | } 88 | 89 | - (NSString *)displayName 90 | { 91 | return title; 92 | } 93 | 94 | - (void)windowControllerDidLoadNib:(NSWindowController *)windowController 95 | { 96 | NSString *sizeString = [[NSUserDefaults standardUserDefaults] stringForKey:@"AproposWindowSize"]; 97 | 98 | [super windowControllerDidLoadNib:windowController]; 99 | 100 | if (sizeString != nil) 101 | { 102 | NSSize windowSize = NSSizeFromString(sizeString); 103 | NSWindow *window = [tableView window]; 104 | NSRect frame = [window frame]; 105 | 106 | if (windowSize.width > 30.0 && windowSize.height > 30.0) { 107 | frame.size = windowSize; 108 | [window setFrame:frame display:NO]; 109 | } 110 | } 111 | 112 | [tableView setTarget:self]; 113 | [tableView setDoubleAction:@selector(openManPages:)]; 114 | [tableView sizeLastColumnToFit]; 115 | } 116 | 117 | - (void)parseOutput:(NSString *)output 118 | { 119 | NSArray *lines = [output componentsSeparatedByString:@"\n"]; 120 | NSUInteger i, count = [lines count]; 121 | 122 | if ([output length] == 0) return; 123 | 124 | lines = [lines sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; 125 | 126 | for (i=0; i= 0) { 159 | NSString *manPage = [titles objectAtIndex:[sender clickedRow]]; 160 | [[ManDocumentController sharedDocumentController] openString:manPage oneWordOnly:YES]; 161 | } 162 | } 163 | 164 | - (NSPrintOperation *)printOperationWithSettings:(NSDictionary *)printSettings 165 | error:(NSError **)outError 166 | { 167 | NSPrintInfo *printInfo = [[[NSPrintInfo alloc] initWithDictionary:printSettings] autorelease]; 168 | return [NSPrintOperation printOperationWithView:tableView 169 | printInfo:printInfo]; 170 | } 171 | 172 | /* NSTableView dataSource */ 173 | - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView 174 | { 175 | return [titles count]; 176 | } 177 | 178 | - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row 179 | { 180 | NSArray *strings = (tableColumn == titleColumn)? titles : descriptions; 181 | return [strings objectAtIndex:row]; 182 | } 183 | 184 | /* Document restoration */ 185 | #define RestoreSearchString @"SearchString" 186 | #define RestoreTitle @"Title" 187 | 188 | - (void)encodeRestorableStateWithCoder:(NSCoder *)coder 189 | { 190 | [super encodeRestorableStateWithCoder:coder]; 191 | [coder encodeObject:searchString forKey:RestoreSearchString]; 192 | [coder encodeObject:title forKey:RestoreTitle]; 193 | } 194 | 195 | - (void)restoreStateWithCoder:(NSCoder *)coder 196 | { 197 | [super restoreStateWithCoder:coder]; 198 | 199 | if (![coder containsValueForKey:RestoreSearchString]) 200 | return; 201 | 202 | NSString *search = [coder decodeObjectForKey:RestoreSearchString]; 203 | NSString *theTitle = [coder decodeObjectForKey:RestoreTitle]; 204 | NSString *manPath = [[NSUserDefaults standardUserDefaults] manPath]; 205 | 206 | [self _loadWithString:search manPath:manPath title:theTitle]; 207 | [[self windowControllers] makeObjectsPerformSelector:@selector(synchronizeWindowTitleWithDocumentName)]; 208 | [tableView reloadData]; 209 | } 210 | 211 | @end 212 | -------------------------------------------------------------------------------- /ManOpen/DisplayPathFormatter.h: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayPathFormatter.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/15/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | /* Formatter to abbreviate folders in the user's home directory for a nicer display. */ 12 | @interface DisplayPathFormatter : NSFormatter 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /ManOpen/DisplayPathFormatter.m: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayPathFormatter.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/15/18. 6 | // 7 | 8 | #import "DisplayPathFormatter.h" 9 | 10 | 11 | @implementation DisplayPathFormatter 12 | 13 | - (NSString *)stringForObjectValue:(id)anObject 14 | { 15 | NSString *new = [anObject stringByAbbreviatingWithTildeInPath]; 16 | 17 | /* The above method may not work if the home directory is a symlink, and our path is already resolved */ 18 | if ([new isAbsolutePath]) 19 | { 20 | static NSString *resHome = nil; 21 | if (resHome == nil) 22 | resHome = [[[NSHomeDirectory() stringByResolvingSymlinksInPath] stringByAppendingString:@"/"] retain]; 23 | 24 | if ([new hasPrefix:resHome]) 25 | new = [@"~/" stringByAppendingString:[new substringFromIndex:[resHome length]]]; 26 | } 27 | 28 | return new; 29 | } 30 | 31 | - (BOOL)getObjectValue:(id *)anObject forString:(NSString *)string errorDescription:(NSString **)error { 32 | *anObject = [string stringByExpandingTildeInPath]; 33 | return YES; 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /ManOpen/FileURLComponents.h: -------------------------------------------------------------------------------- 1 | // 2 | // FileURLComponents.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 1/14/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | @interface FileURLComponents : NSObject 12 | 13 | @property (copy) NSString *host; 14 | @property (copy) NSString *path; 15 | 16 | @property (readonly) BOOL isAbsolute; 17 | @property (readonly) BOOL isDirectory; 18 | @property (readonly) BOOL isLocalhost; 19 | 20 | - (instancetype)initWithHost:(NSString *)host 21 | andPath:(NSString *)path; 22 | 23 | - (instancetype)initWithURL:(NSURL *)url; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /ManOpen/FileURLComponents.m: -------------------------------------------------------------------------------- 1 | // 2 | // FileURLComponents.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 1/14/18. 6 | // 7 | 8 | #import "FileURLComponents.h" 9 | 10 | 11 | @implementation FileURLComponents 12 | 13 | - (instancetype)initWithHost:(NSString *)host 14 | andPath:(NSString *)path 15 | { 16 | self = [super init]; 17 | if (self) { 18 | _host = host.length ? [host copy] : nil; 19 | _path = path.length ? [path copy] : nil; 20 | } 21 | return self; 22 | } 23 | 24 | - (instancetype)initWithURL:(NSURL *)url 25 | { 26 | return [self initWithHost:url.host 27 | andPath:url.path]; 28 | } 29 | 30 | - (void)dealloc 31 | { 32 | [_host release]; 33 | [_path release]; 34 | [super dealloc]; 35 | } 36 | 37 | - (BOOL)isAbsolute 38 | { 39 | return [_path hasPrefix:@"/"]; 40 | } 41 | 42 | - (BOOL)isDirectory 43 | { 44 | return [_path hasSuffix:@"/"]; 45 | } 46 | 47 | - (BOOL)isLocalhost 48 | { 49 | if (_host) { 50 | return [@"localhost" isEqualToString:_host.lowercaseString]; 51 | } else { 52 | return YES; 53 | } 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /ManOpen/Info-ManOpen.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeName 11 | unknown 12 | CFBundleTypeRole 13 | Viewer 14 | LSItemContentTypes 15 | 16 | public.data 17 | 18 | LSTypeIsPackage 19 | 20 | NSDocumentClass 21 | ManDocument 22 | 23 | 24 | CFBundleTypeName 25 | apropos 26 | CFBundleTypeRole 27 | Viewer 28 | NSDocumentClass 29 | AproposDocument 30 | 31 | 32 | CFBundleTypeExtensions 33 | 34 | man 35 | 36 | CFBundleTypeIconFile 37 | ManOpen.icns 38 | CFBundleTypeName 39 | man 40 | CFBundleTypeRole 41 | Viewer 42 | NSDocumentClass 43 | ManDocument 44 | 45 | 46 | CFBundleTypeExtensions 47 | 48 | cat 49 | 50 | CFBundleTypeIconFile 51 | ManOpen.icns 52 | CFBundleTypeName 53 | cat 54 | CFBundleTypeRole 55 | Viewer 56 | NSDocumentClass 57 | ManDocument 58 | 59 | 60 | CFBundleTypeExtensions 61 | 62 | man.gz 63 | 64 | CFBundleTypeIconFile 65 | ManOpen.icns 66 | CFBundleTypeName 67 | mangz 68 | CFBundleTypeRole 69 | Viewer 70 | NSDocumentClass 71 | ManDocument 72 | 73 | 74 | CFBundleTypeExtensions 75 | 76 | cat.gz 77 | 78 | CFBundleTypeIconFile 79 | ManOpen.icns 80 | CFBundleTypeName 81 | catgz 82 | CFBundleTypeRole 83 | Viewer 84 | NSDocumentClass 85 | ManDocument 86 | 87 | 88 | CFBundleExecutable 89 | ManOpen 90 | CFBundleGetInfoString 91 | ManOpen 2.6 (c) 2012 Carl Lindberg 92 | CFBundleIconFile 93 | ManOpen.icns 94 | CFBundleIdentifier 95 | $(PRODUCT_BUNDLE_IDENTIFIER) 96 | CFBundleInfoDictionaryVersion 97 | 6.0 98 | CFBundleName 99 | ManOpen 100 | CFBundlePackageType 101 | APPL 102 | CFBundleShortVersionString 103 | 2.6.1 104 | CFBundleSignature 105 | ???? 106 | CFBundleURLTypes 107 | 108 | 109 | CFBundleTypeRole 110 | Viewer 111 | CFBundleURLName 112 | Man Page URL 113 | CFBundleURLSchemes 114 | 115 | x-man-page 116 | 117 | 118 | 119 | CFBundleTypeRole 120 | Viewer 121 | CFBundleURLName 122 | ManOpen URL 123 | CFBundleURLSchemes 124 | 125 | manopen 126 | 127 | 128 | 129 | CFBundleVersion 130 | 2.6.1 131 | LSApplicationCategoryType 132 | public.app-category.developer-tools 133 | NSAppVersion 134 | Version 2.6 135 | NSAppleScriptEnabled 136 | YES 137 | NSHumanReadableCompleteName 138 | ManOpen 139 | NSHumanReadableCopyright 140 | Copyright (c) 1999-2012 Carl Lindberg 141 | NSHumanReadableShortName 142 | ManOpen 143 | NSMainNibFile 144 | ManOpen 145 | NSPrincipalClass 146 | NSApplication 147 | NSServices 148 | 149 | 150 | NSExecutable 151 | ManOpen 152 | NSMenuItem 153 | 154 | default 155 | ManOpen/Open File 156 | 157 | NSMessage 158 | openFiles 159 | NSPortName 160 | ManOpen 161 | NSSendTypes 162 | 163 | NSFilenamesPboardType 164 | 165 | 166 | 167 | NSExecutable 168 | ManOpen 169 | NSKeyEquivalent 170 | 171 | default 172 | M 173 | 174 | NSMenuItem 175 | 176 | default 177 | ManOpen/Open Selection 178 | 179 | NSMessage 180 | openSelection 181 | NSPortName 182 | ManOpen 183 | NSSendTypes 184 | 185 | NSStringPboardType 186 | 187 | 188 | 189 | NSExecutable 190 | ManOpen 191 | NSKeyEquivalent 192 | 193 | default 194 | A 195 | 196 | NSMenuItem 197 | 198 | default 199 | ManOpen/Apropos 200 | 201 | NSMessage 202 | openApropos 203 | NSPortName 204 | ManOpen 205 | NSSendTypes 206 | 207 | NSStringPboardType 208 | 209 | 210 | 211 | NSSupportsSuddenTermination 212 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /ManOpen/LinkCursor.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/ManOpen/LinkCursor.tiff -------------------------------------------------------------------------------- /ManOpen/MVAppInfo.h: -------------------------------------------------------------------------------- 1 | // 2 | // MVAppInfo.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/15/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | #define URL_SCHEME @"x-man-page" 12 | #define URL_SCHEME_PREFIX URL_SCHEME @":" 13 | 14 | 15 | /* Little class to store info on the possible man page viewers, for easier sorting by display name */ 16 | @interface MVAppInfo : NSObject 17 | { 18 | NSString *bundleID; 19 | NSString *displayName; 20 | NSURL *appURL; 21 | } 22 | 23 | + (NSArray *)allManViewerApps; 24 | + (void)addAppWithID:(NSString *)aBundleID sort:(BOOL)shouldResort; 25 | + (NSUInteger)indexOfBundleID:(NSString*)bundleID; 26 | - (NSString *)bundleID; 27 | - (NSString *)displayName; 28 | - (NSURL *)appURL; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /ManOpen/MVAppInfo.m: -------------------------------------------------------------------------------- 1 | // 2 | // MVAppInfo.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/15/18. 6 | // 7 | 8 | #import "MVAppInfo.h" 9 | 10 | 11 | @implementation MVAppInfo 12 | 13 | static NSMutableArray *allApps = nil; 14 | 15 | - (id)initWithBundleID:(NSString *)aBundleID 16 | { 17 | bundleID = [aBundleID retain]; 18 | return self; 19 | } 20 | 21 | - (void)dealloc 22 | { 23 | [appURL release]; 24 | [displayName release]; 25 | [bundleID release]; 26 | [super dealloc]; 27 | } 28 | 29 | - (BOOL)isEqualToBundleID:(NSString *)aBundleID 30 | { 31 | return [bundleID caseInsensitiveCompare:aBundleID] == NSOrderedSame; 32 | } 33 | - (BOOL)isEqual:(id)other 34 | { 35 | return [self isEqualToBundleID:[other bundleID]]; 36 | } 37 | - (NSUInteger)hash 38 | { 39 | return [[bundleID lowercaseString] hash]; 40 | } 41 | - (NSComparisonResult)compareDisplayName:(id)other 42 | { 43 | return [[self displayName] localizedCaseInsensitiveCompare:[other displayName]]; 44 | } 45 | 46 | - (NSString *)bundleID 47 | { 48 | return bundleID; 49 | } 50 | 51 | - (NSURL *)appURL 52 | { 53 | if (appURL == nil) 54 | { 55 | NSString *path = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:bundleID]; 56 | if (path != nil) 57 | appURL = [[NSURL fileURLWithPath:path] retain]; 58 | } 59 | 60 | return appURL; 61 | } 62 | 63 | - (NSString *)displayName 64 | { 65 | if (displayName == nil) 66 | { 67 | NSURL *url = [self appURL]; 68 | NSDictionary *infoDict = [(id)CFBundleCopyInfoDictionaryForURL((CFURLRef)url) autorelease]; 69 | NSString *appVersion; 70 | NSString *niceName = nil; 71 | 72 | if (infoDict == nil) 73 | infoDict = [[NSBundle bundleWithPath:[url path]] infoDictionary]; 74 | 75 | LSCopyDisplayNameForURL((CFURLRef)url, (CFStringRef*)&niceName); 76 | [niceName autorelease]; 77 | if (niceName == nil) 78 | niceName = [[url path] lastPathComponent]; 79 | 80 | appVersion = [infoDict objectForKey:@"CFBundleShortVersionString"]; 81 | if (appVersion != nil) 82 | niceName = [NSString stringWithFormat:@"%@ (%@)", niceName, appVersion]; 83 | 84 | displayName = [niceName retain]; 85 | } 86 | 87 | return displayName; 88 | } 89 | 90 | + (void)sortApps 91 | { 92 | [allApps sortUsingSelector:@selector(compareDisplayName:)]; 93 | } 94 | + (void)addAppWithID:(NSString *)aBundleID sort:(BOOL)shouldResort 95 | { 96 | MVAppInfo *info = [[MVAppInfo alloc] initWithBundleID:aBundleID]; 97 | if ([info appURL] && ![allApps containsObject:info]) 98 | { 99 | [allApps addObject:info]; 100 | if (shouldResort) 101 | [self sortApps]; 102 | } 103 | [info release]; 104 | } 105 | 106 | + (NSArray *)allManViewerApps 107 | { 108 | if (allApps == nil) 109 | { 110 | /* Ensure our app is registered */ 111 | // NSString *appPath = [[NSBundle mainBundle] bundlePath]; 112 | // NSURL *url = [NSURL fileURLWithPath:appPath]; 113 | // LSRegisterURL((CFURLRef)url, false); 114 | 115 | NSArray *allBundleIDs = [(id)LSCopyAllHandlersForURLScheme((CFStringRef)URL_SCHEME) autorelease]; 116 | NSUInteger i; 117 | 118 | allApps = [[NSMutableArray alloc] initWithCapacity:[allBundleIDs count]]; 119 | for (i = 0; i<[allBundleIDs count]; i++) { 120 | [self addAppWithID:[allBundleIDs objectAtIndex:i] sort:NO]; 121 | } 122 | [self sortApps]; 123 | } 124 | 125 | return allApps; 126 | } 127 | 128 | + (NSUInteger)indexOfBundleID:(NSString*)bundleID 129 | { 130 | NSArray *apps = [self allManViewerApps]; 131 | NSUInteger i, count = [apps count]; 132 | 133 | for (i=0; bundleID != nil && i 4 | #import 5 | 6 | @class NSMutableArray, NSMutableDictionary; 7 | @class ManTextView; 8 | @class NSTextField, NSText, NSButton, NSPopUpButton; 9 | 10 | @interface ManDocument : NSDocument 11 | { 12 | NSString *shortTitle; 13 | NSData *taskData; 14 | BOOL hasLoaded; 15 | NSURL *copyURL; 16 | NSMutableArray *sections; 17 | NSMutableArray *sectionRanges; 18 | NSMutableDictionary *restoreData; 19 | 20 | IBOutlet ManTextView *textView; 21 | IBOutlet NSTextField *titleStringField; 22 | IBOutlet NSButton *openSelectionButton; 23 | IBOutlet NSPopUpButton *sectionPopup; 24 | } 25 | 26 | - initWithName:(NSString *)name 27 | section:(NSString *)section 28 | manPath:(NSString *)manPath 29 | title:(NSString *)title; 30 | 31 | - (NSString *)shortTitle; 32 | - (void)setShortTitle:(NSString *)aString; 33 | 34 | - (NSText *)textView; 35 | 36 | - (void)loadCommand:(NSString *)command; 37 | 38 | - (IBAction)saveCurrentWindowSize:(id)sender; 39 | - (IBAction)openSelection:(id)sender; 40 | - (IBAction)displaySection:(id)sender; 41 | - (IBAction)copyURL:(id)sender; 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /ManOpen/ManDocumentController.h: -------------------------------------------------------------------------------- 1 | 2 | #import "SystemType.h" 3 | #import 4 | 5 | 6 | @class NSPanel, NSTextField, NSPopUpButton, NSFont; 7 | @class NSData, NSMutableString; 8 | 9 | 10 | @interface ManDocumentController : NSDocumentController 11 | { 12 | IBOutlet NSPanel *openTextPanel; 13 | IBOutlet NSPanel *aproposPanel; 14 | IBOutlet NSPanel *infoPanel; 15 | IBOutlet NSPanel *helpPanel; 16 | IBOutlet NSTextField *aproposField; 17 | IBOutlet NSTextField *openTextField; 18 | IBOutlet NSPopUpButton *openSectionPopup; 19 | BOOL startedUp; 20 | NSArray *docControllerObjects; 21 | } 22 | 23 | - (id)openWord:(NSString *)word; 24 | 25 | - (void)openString:(NSString *)string; 26 | - (void)openString:(NSString *)string oneWordOnly:(BOOL)oneOnly; 27 | 28 | - (void)openFile:(NSString *)filename 29 | forceToFront:(BOOL)force; 30 | 31 | - (void)openName:(NSString *)name 32 | section:(NSString *)section 33 | manPath:(NSString *)manPath 34 | forceToFront:(BOOL)force; 35 | 36 | - (void)openApropos:(NSString *)apropos; 37 | 38 | - (void)openApropos:(NSString *)apropos 39 | manPath:(NSString *)manPath 40 | forceToFront:(BOOL)force; 41 | 42 | - (IBAction)openSection:(id)sender; 43 | - (IBAction)openTextPanel:(id)sender; 44 | - (IBAction)openAproposPanel:(id)sender; 45 | - (IBAction)okApropos:(id)sender; 46 | - (IBAction)okText:(id)sender; 47 | - (IBAction)cancelText:(id)sender; 48 | 49 | - (IBAction)orderFrontHelpPanel:(id)sender; 50 | - (IBAction)orderFrontPreferencesPanel:(id)sender; 51 | 52 | // Helper methods for document classes 53 | - (NSMutableString *)manCommandWithManPath:(NSString *)manPath; 54 | - (NSData *)dataByExecutingCommand:(NSString *)command; 55 | - (NSData *)dataByExecutingCommand:(NSString *)command manPath:(NSString *)manPath; 56 | - (NSString *)typeFromFilename:(NSString *)filename; 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /ManOpen/ManOpen.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/ManOpen/ManOpen.icns -------------------------------------------------------------------------------- /ManOpen/ManOpen.scriptSuite: -------------------------------------------------------------------------------- 1 | { 2 | Name = ManOpen; 3 | AppleEventCode = "Mopn"; 4 | 5 | Commands = { 6 | "GetURL" = { 7 | CommandClass = ManOpenURLHandlerCommand; 8 | AppleEventCode = GURL; 9 | AppleEventClassCode = GURL; 10 | }; 11 | }; 12 | } -------------------------------------------------------------------------------- /ManOpen/ManOpen.scriptTerminology: -------------------------------------------------------------------------------- 1 | { 2 | Name = "ManOpen commands"; 3 | Description = "Commands to handle a URL"; 4 | 5 | Commands = { 6 | "GetURL" = { 7 | "Name" = "GetURL"; 8 | "Description" = "Opens a manopen:, x-man-page: or file: URL"; 9 | }; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /ManOpen/ManOpen.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/ManOpen/ManOpen.tiff -------------------------------------------------------------------------------- /ManOpen/ManOpenURLComponents.h: -------------------------------------------------------------------------------- 1 | // 2 | // ManOpenURLComponents.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/3/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | @class ManPage; 12 | 13 | 14 | @interface ManOpenURLComponents : NSObject 15 | 16 | @property (copy) NSString *aproposKeyword; 17 | @property (copy) NSString *filePath; 18 | @property (assign) BOOL isBackground; 19 | @property (retain) ManPage *manPage; 20 | @property (readonly) NSString *manPath; 21 | @property (copy) NSArray *manPathArray; 22 | @property (readonly) NSURL *url; 23 | 24 | - (instancetype)initWithAproposKeyword:(NSString *)aproposKeyword 25 | manPath:(NSString *)manPath 26 | isBackground:(BOOL)isBackground; 27 | 28 | - (instancetype)initWithAproposKeyword:(NSString *)aproposKeyword 29 | manPathArray:(NSArray *)manPathArray 30 | isBackground:(BOOL)isBackground; 31 | 32 | - (instancetype)initWithFilePath:(NSString *)filePath 33 | isBackground:(BOOL)isBackground; 34 | 35 | - (instancetype)initWithManPage:(ManPage *)manPage 36 | manPathArray:(NSArray *)manPathArray 37 | isBackground:(BOOL)isBackground; 38 | 39 | - (instancetype)initWithSection:(NSString *)section 40 | name:(NSString *)name 41 | manPath:(NSString *)manPath 42 | isBackground:(BOOL)isBackground; 43 | 44 | - (instancetype)initWithURL:(NSURL *)url; 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /ManOpen/ManOpenURLComponents.m: -------------------------------------------------------------------------------- 1 | // 2 | // ManOpenURLComponents.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/3/18. 6 | // 7 | 8 | #import "ManOpenURLComponents.h" 9 | 10 | #import "ManPage.h" 11 | #import "NSDictionary+ManOpen.h" 12 | #import "NSURL+ManOpen.h" 13 | 14 | 15 | @implementation ManOpenURLComponents 16 | 17 | - (instancetype)initWithAproposKeyword:(NSString *)aproposKeyword 18 | manPath:(NSString *)manPath 19 | isBackground:(BOOL)isBackground 20 | { 21 | return [self initWithAproposKeyword:aproposKeyword 22 | manPathArray:[manPath componentsSeparatedByString:@":"] 23 | isBackground:isBackground]; 24 | } 25 | 26 | - (instancetype)initWithAproposKeyword:(NSString *)aproposKeyword 27 | manPathArray:(NSArray *)manPathArray 28 | isBackground:(BOOL)isBackground 29 | { 30 | self = [super init]; 31 | if (self) { 32 | _aproposKeyword = aproposKeyword.length ? [aproposKeyword copy] : nil; 33 | _manPathArray = manPathArray.count ? [manPathArray copy] : nil; 34 | _isBackground = isBackground; 35 | } 36 | return self; 37 | } 38 | 39 | - (instancetype)initWithFilePath:(NSString *)filePath 40 | isBackground:(BOOL)isBackground 41 | { 42 | self = [super init]; 43 | if (self) { 44 | _filePath = filePath.length ? [filePath copy] : nil; 45 | _isBackground = isBackground; 46 | } 47 | return self; 48 | } 49 | 50 | - (instancetype)initWithManPage:(ManPage *)manPage 51 | manPathArray:(NSArray *)manPathArray 52 | isBackground:(BOOL)isBackground 53 | { 54 | self = [super init]; 55 | if (self) { 56 | _manPage = [manPage retain]; 57 | _manPathArray = manPathArray.count ? [manPathArray copy] : nil; 58 | _isBackground = isBackground; 59 | } 60 | return self; 61 | } 62 | 63 | - (instancetype)initWithSection:(NSString *)section 64 | name:(NSString *)name 65 | manPath:(NSString *)manPath 66 | isBackground:(BOOL)isBackground 67 | { 68 | ManPage *manPage = [[ManPage alloc] initWithSection:section 69 | andName:name]; 70 | [manPage autorelease]; 71 | return [self initWithManPage:manPage 72 | manPathArray:[manPath componentsSeparatedByString:@":"] 73 | isBackground:isBackground]; 74 | } 75 | 76 | - (instancetype)initWithURL:(NSURL *)url 77 | { 78 | if (![url isManOpenScheme]) return nil; 79 | 80 | NSPredicate *notRootPredicate = [NSPredicate predicateWithFormat:@"'/' != SELF"]; 81 | NSArray *pathComponents = [url.pathComponents filteredArrayUsingPredicate:notRootPredicate]; 82 | if (!pathComponents.count) return nil; 83 | 84 | NSDictionary *query = [NSDictionary dictionaryWithURLQuery:url.query]; 85 | BOOL isBackground = [@"true" isEqual:query[@"background"].lowercaseString]; 86 | 87 | NSUInteger schemeEnd = @"manopen:".length; 88 | NSString *resource = [url.absoluteString substringFromIndex:schemeEnd]; 89 | if ([resource hasPrefix:@"//"]) { 90 | NSString *section = url.host.length ? url.host : @""; 91 | NSString *name = pathComponents.firstObject; 92 | 93 | NSArray *manPathArray = [query[@"MANPATH"] componentsSeparatedByString:@":"]; 94 | 95 | if ([@"apropos" isEqualToString:section.lowercaseString]) { 96 | return [self initWithAproposKeyword:name 97 | manPathArray:manPathArray 98 | isBackground:isBackground]; 99 | } else { 100 | ManPage *manPage = [[ManPage alloc] initWithSection:section 101 | andName:name]; 102 | [manPage autorelease]; 103 | return [self initWithManPage:manPage 104 | manPathArray:manPathArray 105 | isBackground:isBackground]; 106 | } 107 | } else { 108 | return [self initWithFilePath:url.path 109 | isBackground:isBackground]; 110 | } 111 | } 112 | 113 | - (void)dealloc 114 | { 115 | [_aproposKeyword release]; 116 | [_filePath release]; 117 | [_manPage release]; 118 | [_manPathArray release]; 119 | [super dealloc]; 120 | } 121 | 122 | - (NSString *)manPath 123 | { 124 | return [_manPathArray componentsJoinedByString:@":"]; 125 | } 126 | 127 | - (NSURL *)url 128 | { 129 | NSString *urlString = nil; 130 | NSMutableDictionary *query = [[NSMutableDictionary new] autorelease]; 131 | if (_aproposKeyword) { 132 | urlString = [NSString stringWithFormat:@"manopen://apropos/%@", _aproposKeyword]; 133 | if (_manPathArray) query[@"MANPATH"] = self.manPath; 134 | } else if (_manPage) { 135 | urlString = [NSString stringWithFormat:@"manopen://%@/%@", 136 | _manPage.section ?: @"", _manPage.name]; 137 | if (_manPathArray) query[@"MANPATH"] = self.manPath; 138 | } else if (_filePath) { 139 | urlString = [NSString stringWithFormat:@"manopen:%@", _filePath]; 140 | } 141 | if (urlString) { 142 | if (_isBackground) query[@"background"] = @"true"; 143 | urlString = [urlString stringByAppendingString:query.urlQuery]; 144 | return [NSURL URLWithString:urlString]; 145 | } else { 146 | return nil; 147 | } 148 | } 149 | 150 | @end 151 | -------------------------------------------------------------------------------- /ManOpen/ManOpenURLHandlerCommand.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | 4 | @class ManDocumentController; 5 | 6 | 7 | @interface ManOpenURLHandlerCommand : NSScriptCommand 8 | 9 | @property (retain) ManDocumentController *manDocumentController; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /ManOpen/ManOpenURLHandlerCommand.m: -------------------------------------------------------------------------------- 1 | #import "ManOpenURLHandlerCommand.h" 2 | 3 | #import "FileURLComponents.h" 4 | #import "ManDocumentController.h" 5 | #import "ManOpenURLComponents.h" 6 | #import "ManPage.h" 7 | #import "NSURL+ManOpen.h" 8 | #import "XManPageURLComponents.h" 9 | 10 | 11 | @implementation ManOpenURLHandlerCommand 12 | 13 | - (instancetype)init 14 | { 15 | self = [super init]; 16 | if (self) { 17 | _manDocumentController = [[ManDocumentController sharedDocumentController] retain]; 18 | } 19 | return self; 20 | } 21 | 22 | - (instancetype)initWithCommandDescription:(NSScriptCommandDescription *)commandDef 23 | { 24 | self = [super initWithCommandDescription:commandDef]; 25 | if (self) { 26 | _manDocumentController = [[ManDocumentController sharedDocumentController] retain]; 27 | } 28 | return self; 29 | } 30 | 31 | - (void)dealloc 32 | { 33 | [_manDocumentController release]; 34 | [super dealloc]; 35 | } 36 | 37 | - (id)performDefaultImplementation 38 | { 39 | NSURL *url = [NSURL URLWithString:self.directParameter]; 40 | 41 | if (url.isManOpenScheme) { 42 | ManOpenURLComponents *components = [[[ManOpenURLComponents alloc] initWithURL:url] autorelease]; 43 | if (components.manPage) { 44 | [_manDocumentController openName:components.manPage.name 45 | section:components.manPage.section 46 | manPath:components.manPath 47 | forceToFront:!components.isBackground]; 48 | } else if (components.aproposKeyword) { 49 | [_manDocumentController openApropos:components.aproposKeyword 50 | manPath:components.manPath 51 | forceToFront:!components.isBackground]; 52 | } else if (components.filePath) { 53 | [_manDocumentController openFile:components.filePath 54 | forceToFront:!components.isBackground]; 55 | } 56 | } else if (url.isXManPageScheme) { 57 | XManPageURLComponents *components = [[[XManPageURLComponents alloc] initWithURL:url] autorelease]; 58 | if (components.aproposKeyword) { 59 | [_manDocumentController openApropos:components.aproposKeyword 60 | manPath:nil 61 | forceToFront:YES]; 62 | } else if (components.manPages.count) { 63 | for (ManPage *manPage in components.manPages) { 64 | [_manDocumentController openName:manPage.name 65 | section:manPage.section 66 | manPath:nil 67 | forceToFront:YES]; 68 | } 69 | } 70 | } else if (url.isFileScheme) { 71 | FileURLComponents *components = [[[FileURLComponents alloc] initWithURL:url] autorelease]; 72 | if (components.isLocalhost && components.isAbsolute && !components.isDirectory) { 73 | [_manDocumentController openFile:components.path 74 | forceToFront:YES]; 75 | } 76 | } 77 | 78 | return nil; 79 | } 80 | 81 | @end 82 | -------------------------------------------------------------------------------- /ManOpen/ManOpen_main.m: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | int main(int argc, const char *argv[]) { 5 | return NSApplicationMain(argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /ManOpen/ManPage.h: -------------------------------------------------------------------------------- 1 | // 2 | // ManPage.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 1/20/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | @interface ManPage : NSObject 12 | 13 | @property (copy) NSString *name; 14 | @property (copy) NSString *section; 15 | 16 | + (BOOL)isSection:(NSString *)section; 17 | 18 | - (instancetype)initWithSection:(NSString *)section 19 | andName:(NSString *)name; 20 | 21 | - (instancetype)initWithName:(NSString *)name; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /ManOpen/ManPage.m: -------------------------------------------------------------------------------- 1 | // 2 | // ManPage.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 1/20/18. 6 | // 7 | 8 | #import "ManPage.h" 9 | 10 | 11 | @implementation ManPage 12 | 13 | + (BOOL)isSection:(NSString *)section 14 | { 15 | if (!section.length) return NO; 16 | 17 | if ([@"n" isEqualToString:section]) return YES; 18 | if ([@"x" isEqualToString:section]) return YES; 19 | 20 | unichar firstCharacter = [section characterAtIndex:0]; 21 | if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:firstCharacter]) return YES; 22 | 23 | return NO; 24 | } 25 | 26 | - (instancetype)init 27 | { 28 | return [self initWithSection:nil 29 | andName:nil]; 30 | } 31 | 32 | - (instancetype)initWithSection:(NSString *)section 33 | andName:(NSString *)name 34 | { 35 | self = [super init]; 36 | if (self) { 37 | _name = name.length ? [name copy] : nil; 38 | _section = [[self class] isSection:section] ? [section copy] : nil; 39 | } 40 | return self; 41 | } 42 | 43 | - (instancetype)initWithName:(NSString *)name 44 | { 45 | return [self initWithSection:nil 46 | andName:name]; 47 | } 48 | 49 | - (void)dealloc 50 | { 51 | [_name release]; 52 | [_section release]; 53 | [super dealloc]; 54 | } 55 | 56 | - (NSString *)description 57 | { 58 | if (_section && _name) { 59 | return [NSString stringWithFormat:@"%@(%@)", _name, _section]; 60 | } else if (_name) { 61 | return _name; 62 | } else { 63 | return @""; 64 | } 65 | } 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /ManOpen/NSData+Utils.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | @interface NSData (Utils) 5 | 6 | - (BOOL)isNroffData; 7 | - (BOOL)isRTFData; 8 | - (BOOL)isGzipData; 9 | - (BOOL)isBinaryData; 10 | 11 | @end 12 | 13 | #import 14 | 15 | @interface NSFileHandle (Utils) 16 | 17 | - (NSData *)readDataToEndOfFileIgnoreInterrupt; 18 | 19 | @end 20 | 21 | -------------------------------------------------------------------------------- /ManOpen/NSData+Utils.m: -------------------------------------------------------------------------------- 1 | 2 | #import "NSData+Utils.h" 3 | #import "SystemType.h" 4 | #import 5 | #import 6 | #import 7 | 8 | @implementation NSData (Utils) 9 | 10 | /* 11 | * Checks the data to see if it looks like the start of an nroff file. 12 | * Derived from logic in FreeBSD's file(1) command. 13 | */ 14 | - (BOOL)isNroffData 15 | { 16 | const char *bytes = [self bytes]; 17 | const char *ptr = bytes; 18 | 19 | #define MATCH(str) (strncmp(ptr, str, strlen(str)) == 0) 20 | 21 | while (isspace(*ptr)) ptr++; 22 | 23 | /* Some X11R6 pages have a weird #pragma line at the start */ 24 | if (MATCH("#pragma")) 25 | { 26 | const char *nextline = strchr(ptr, '\n'); 27 | if (nextline != NULL) { 28 | ptr = nextline; 29 | while (isspace(*ptr)) ptr++; 30 | } 31 | } 32 | 33 | /* If not at the beginning of a line, bail. */ 34 | if (!(ptr == bytes || *(ptr-1) == '\n' || *(ptr-1) == '\r')) return NO; 35 | 36 | 37 | /* Try for some common prefixes: .\", '\", '.\", \", and .\ */ 38 | if (MATCH(".\\\"")) return YES; 39 | if (MATCH("'\\\"")) return YES; 40 | if (MATCH("'.\\\"")) return YES; 41 | if (MATCH("\\\"")) return YES; 42 | if (MATCH(".\\ ")) return YES; 43 | if (MATCH("\\.\"")) return YES; // found this on a joke man page 44 | 45 | /* 46 | * Now check for .[letter][letter], and .\" again. In either case, 47 | * allow spaces after the '.' 48 | */ 49 | if (*ptr == '.') 50 | { 51 | /* skip over '.' and whitespace */ 52 | ptr++; 53 | while (isspace(*ptr)) ptr++; 54 | 55 | if (isalnum(ptr[0]) && isalnum(ptr[1])) return YES; 56 | if (ptr[0] == '\\' && ptr[1] == '"') return YES; 57 | } 58 | 59 | return NO; 60 | } 61 | 62 | - (BOOL)hasPrefixBytes:(void *)bytes length:(NSUInteger)len 63 | { 64 | if ([self length] < len) return NO; 65 | return (memcmp([self bytes], bytes, (size_t)len) == 0); 66 | } 67 | 68 | - (BOOL)isRTFData 69 | { 70 | char *header = "{\\rtf"; 71 | return [self hasPrefixBytes:header length:strlen(header)]; 72 | } 73 | 74 | - (BOOL)isGzipData 75 | { 76 | return ([self hasPrefixBytes:"\037\235" length:2] || // compress(1) header 77 | [self hasPrefixBytes:"\037\213" length:2]); // gzip(1) header 78 | } 79 | 80 | /* Very rough check -- see if more than a third of the first 100 bytes have the high bit set */ 81 | - (BOOL)isBinaryData 82 | { 83 | NSUInteger checklen = MIN((NSUInteger)100, [self length]); 84 | NSUInteger i; 85 | NSUInteger badByteCount = 0; 86 | unsigned const char *bytes = [self bytes]; 87 | 88 | if (checklen == 0) return NO; 89 | for (i=0; i 0) && (checklen / badByteCount) <= 2; 93 | } 94 | 95 | @end 96 | 97 | #import 98 | #import 99 | #import 100 | 101 | @implementation NSFileHandle (Utils) 102 | 103 | /* 104 | * The NSData -readDataToEndOfFile method does not deal with EINTR errors, which in most 105 | * cases is fine, but sometimes not when running under a debugger. So... this is more to help 106 | * folks working on the code, rather the users ;-) 107 | */ 108 | - (NSData *)readDataToEndOfFileIgnoreInterrupt 109 | { 110 | int fd = [self fileDescriptor]; 111 | size_t offset = 0; 112 | size_t allocated = 16384; 113 | unsigned char *bytes = malloc(allocated); 114 | ssize_t bytesRead; 115 | 116 | do { 117 | if (offset >= allocated) { 118 | allocated *= 2; 119 | bytes = reallocf(bytes, allocated); 120 | } 121 | assert(bytes != NULL); 122 | 123 | do { 124 | bytesRead = read(fd, bytes + offset, allocated - offset); 125 | } while (bytesRead < 0 && ((errno == EINTR) || (errno == EAGAIN) || (errno == EWOULDBLOCK))); 126 | 127 | if (bytesRead > 0) { 128 | offset += bytesRead; 129 | } 130 | 131 | } while (bytesRead > 0); 132 | 133 | if (bytesRead < 0) { 134 | free(bytes); 135 | [NSException raise:NSFileHandleOperationException format:@"%s: %s", __FUNCTION__, strerror(errno)]; 136 | } 137 | 138 | if (offset) { 139 | bytes = reallocf(bytes, offset); 140 | return [NSData dataWithBytesNoCopy:bytes length:(NSUInteger)offset]; 141 | } else { 142 | return [NSData data]; 143 | } 144 | } 145 | 146 | @end 147 | -------------------------------------------------------------------------------- /ManOpen/NSDictionary+ManOpen.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+ManOpen.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 12/25/17. 6 | // 7 | 8 | #import 9 | 10 | 11 | @interface NSDictionary (ManOpen) 12 | 13 | @property (readonly) NSString *urlQuery; 14 | 15 | + (NSDictionary *)dictionaryWithURLQuery:(NSString *)urlQuery; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /ManOpen/NSDictionary+ManOpen.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+ManOpen.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 12/25/17. 6 | // 7 | 8 | #import "NSDictionary+ManOpen.h" 9 | 10 | 11 | @implementation NSDictionary (ManOpen) 12 | 13 | + (NSDictionary *)dictionaryWithURLQuery:(NSString *)urlQuery 14 | { 15 | if (!urlQuery) { 16 | return nil; 17 | } 18 | 19 | urlQuery = [urlQuery stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 20 | NSArray *parts = [urlQuery componentsSeparatedByString:@"&"]; 21 | NSMutableDictionary *dictionary = [[NSMutableDictionary new] autorelease]; 22 | for (NSString *part in parts) { 23 | NSRange range = [part rangeOfString:@"="]; 24 | if (NSNotFound == range.location) { 25 | if (part.length) dictionary[part] = @""; 26 | } else { 27 | NSString *key = [part substringToIndex:range.location]; 28 | key = key.stringByRemovingPercentEncoding ?: key; 29 | key = [key stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 30 | NSString *value = [part substringFromIndex:range.location + 1]; 31 | value = value.stringByRemovingPercentEncoding ?: value; 32 | value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 33 | dictionary[key] = value; 34 | } 35 | } 36 | return [[dictionary copy] autorelease]; 37 | } 38 | 39 | - (NSString *)urlQuery 40 | { 41 | if (!self.count) return @""; 42 | 43 | NSMutableString *urlQuery = [NSMutableString stringWithString:@"?"]; 44 | NSArray *sortedKeys = [self.allKeys sortedArrayUsingSelector:@selector(compare:)]; 45 | for (NSString *key in sortedKeys) { 46 | if (urlQuery.length > 1) [urlQuery appendString:@"&"]; 47 | NSString *encodedKey = [key stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet alphanumericCharacterSet]]; 48 | NSString *encodedValue = [self[key] stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet alphanumericCharacterSet]]; 49 | [urlQuery appendFormat:@"%@=%@", encodedKey, encodedValue]; 50 | } 51 | return urlQuery; 52 | } 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /ManOpen/NSObject+PoofDragDataSource.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+PoofDragDataSource.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/15/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | @interface NSObject (PoofDragDataSource) 12 | 13 | - (BOOL)tableView:(NSTableView *)tableView performDropOutsideViewAtPoint:(NSPoint)screenPoint; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ManOpen/NSString+ManOpen.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+ManOpen.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/10/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | @interface NSString (ManOpen) 12 | 13 | @property (readonly) NSArray *manPageWords; 14 | @property (readonly) NSString *singleQuotedShellWord; 15 | @property (readonly) NSArray *wordsSeparatedByWhitespaceAndNewlineCharacters; 16 | 17 | - (NSString *)singleQuotedShellWordWithSurroundingQuotes:(BOOL)addSurroundingQuotes; 18 | 19 | - (NSString *)stringByRemovingSuffix:(NSString *)suffix; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /ManOpen/NSString+ManOpen.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+ManOpen.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/10/18. 6 | // 7 | 8 | #import "NSString+ManOpen.h" 9 | 10 | #import "ManDocumentController.h" 11 | 12 | 13 | @implementation NSString (ManOpen) 14 | 15 | - (NSArray *)manPageWords 16 | { 17 | NSArray *words = self.wordsSeparatedByWhitespaceAndNewlineCharacters; 18 | NSString *lastWord = nil; 19 | NSMutableArray *manPageWords = [[NSMutableArray new] autorelease]; 20 | for (NSString *word in words) { 21 | if (!lastWord) { 22 | lastWord = [word stringByRemovingSuffix:@","]; 23 | } else if ([word hasPrefix:@"("] && [word hasSuffix:@")"]) { 24 | NSString *manPageWord = [lastWord stringByAppendingString:word]; 25 | [manPageWords addObject:manPageWord]; 26 | lastWord = nil; 27 | } else if ([lastWord hasSuffix:@"-"]) { 28 | NSString *prefixWord = [lastWord stringByRemovingSuffix:@"-"]; 29 | lastWord = [prefixWord stringByAppendingString:word]; 30 | } else { 31 | [manPageWords addObject:lastWord]; 32 | lastWord = [word stringByRemovingSuffix:@","]; 33 | } 34 | } 35 | if (lastWord) { 36 | [manPageWords addObject:lastWord]; 37 | } 38 | return manPageWords; 39 | } 40 | 41 | - (NSString *)singleQuotedShellWord 42 | { 43 | return [self singleQuotedShellWordWithSurroundingQuotes:YES]; 44 | } 45 | 46 | - (NSString *)singleQuotedShellWordWithSurroundingQuotes:(BOOL)addSurroundingQuotes 47 | { 48 | NSString *escaped = [self stringByReplacingOccurrencesOfString:@"'" 49 | withString:@"'\\''"]; 50 | if (addSurroundingQuotes) { 51 | return [NSString stringWithFormat:@"'%@'", escaped]; 52 | } else { 53 | return escaped; 54 | } 55 | } 56 | 57 | - (NSString *)stringByRemovingSuffix:(NSString *)suffix 58 | { 59 | if ([self hasSuffix:suffix]) { 60 | return [self substringToIndex:self.length - suffix.length]; 61 | } else { 62 | return [[self retain] autorelease]; 63 | } 64 | } 65 | 66 | - (NSArray *)wordsSeparatedByWhitespaceAndNewlineCharacters 67 | { 68 | NSArray *components = [self componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 69 | NSPredicate *notEmptyPredicate = [NSPredicate predicateWithFormat:@"length != 0"]; 70 | return [components filteredArrayUsingPredicate:notEmptyPredicate]; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /ManOpen/NSURL+ManOpen.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+ManOpen.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 12/24/17. 6 | // 7 | 8 | #import 9 | 10 | 11 | @interface NSURL (ManOpen) 12 | 13 | @property (readonly) BOOL isFileScheme; 14 | @property (readonly) BOOL isManOpenScheme; 15 | @property (readonly) BOOL isXManPageScheme; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /ManOpen/NSURL+ManOpen.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+ManOpen.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 12/24/17. 6 | // 7 | 8 | #import "NSURL+ManOpen.h" 9 | 10 | 11 | @implementation NSURL (ManOpen) 12 | 13 | - (BOOL)isFileScheme 14 | { 15 | return [@"file" isEqualToString:self.scheme.lowercaseString]; 16 | } 17 | 18 | - (BOOL)isManOpenScheme 19 | { 20 | return [@"manopen" isEqualToString:self.scheme.lowercaseString]; 21 | } 22 | 23 | - (BOOL)isXManPageScheme 24 | { 25 | return [@"x-man-page" isEqualToString:self.scheme.lowercaseString]; 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /ManOpen/NSUserDefaults+ManOpen.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSUserDefaults+ManOpen.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/10/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | @interface NSUserDefaults (ManOpen) 12 | 13 | - (NSColor *)colorForKey:(NSString *)key; 14 | 15 | - (NSFont *)manFont; 16 | 17 | - (NSString *)manPath; 18 | 19 | - (NSColor *)manTextColor; 20 | 21 | - (NSColor *)manLinkColor; 22 | 23 | - (NSColor *)manBackgroundColor; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /ManOpen/NSUserDefaults+ManOpen.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSUserDefaults+ManOpen.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/10/18. 6 | // 7 | 8 | #import "NSUserDefaults+ManOpen.h" 9 | 10 | 11 | @implementation NSUserDefaults (ManOpen) 12 | 13 | - (NSColor *)colorForKey:(NSString *)key 14 | { 15 | NSData *colorData = [self dataForKey:key]; 16 | return colorData ? [NSUnarchiver unarchiveObjectWithData:colorData] : nil; 17 | } 18 | 19 | - (NSColor *)manTextColor 20 | { 21 | return [self colorForKey:@"ManTextColor"]; 22 | } 23 | 24 | - (NSColor *)manLinkColor 25 | { 26 | return [self colorForKey:@"ManLinkColor"]; 27 | } 28 | 29 | - (NSColor *)manBackgroundColor 30 | { 31 | return [self colorForKey:@"ManBackgroundColor"]; 32 | } 33 | 34 | - (NSFont *)manFont 35 | { 36 | NSFont *defaultFont = [NSFont userFixedPitchFontOfSize:12.0f]; 37 | 38 | NSString *fontString = [self stringForKey:@"ManFont"]; 39 | if (!fontString) return defaultFont; 40 | 41 | NSRange range = [fontString rangeOfString:@" "]; 42 | if (NSNotFound == range.location) return defaultFont; 43 | 44 | CGFloat size = [fontString substringToIndex:range.location].floatValue ?: 12.0; 45 | NSString *name = [fontString substringFromIndex:NSMaxRange(range)]; 46 | NSFont *font = [NSFont fontWithName:name 47 | size:size]; 48 | 49 | return font ?: defaultFont; 50 | } 51 | 52 | - (NSString *)manPath 53 | { 54 | return [self stringForKey:@"ManPath"]; 55 | } 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /ManOpen/PoofDragTableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // PoofDragTableView.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/15/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | /* 12 | * Class to add a delegate method for when something was dropped with no other action 13 | * outside the view, i.e. the "poof" removing functionality. In 10.7, this can almost 14 | * be implemented in the dataSource, but I wanted to retain the "slide back" functionality 15 | * when dropped in an invalid place inside the view, which requires a subclass anyways. 16 | * Prior to 10.7, a subclass is required to get the "end" notification, and also to 17 | * disable the "slide back" functionality. 18 | */ 19 | @interface PoofDragTableView : NSTableView 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /ManOpen/PoofDragTableView.m: -------------------------------------------------------------------------------- 1 | // 2 | // PoofDragTableView.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/15/18. 6 | // 7 | 8 | #import "PoofDragTableView.h" 9 | 10 | #import "NSObject+PoofDragDataSource.h" 11 | #import "SystemType.h" 12 | 13 | 14 | @implementation PoofDragTableView 15 | 16 | - (BOOL)containsScreenPoint:(NSPoint)screenPoint 17 | { 18 | NSRect screenRect = { 19 | .origin=screenPoint, 20 | }; 21 | NSRect windowRect = [self.window convertRectFromScreen:screenRect]; 22 | NSPoint viewPoint = [self convertPoint:windowRect.origin fromView:nil]; 23 | 24 | return NSMouseInRect(viewPoint, [self bounds], [self isFlipped]); 25 | } 26 | 27 | - (void)draggingSession:(NSDraggingSession *)session 28 | movedToPoint:(NSPoint)screenPoint 29 | { 30 | session.animatesToStartingPositionsOnCancelOrFail = [self containsScreenPoint:screenPoint]; 31 | } 32 | 33 | - (void)draggingSession:(NSDraggingSession *)session 34 | endedAtPoint:(NSPoint)screenPoint 35 | operation:(NSDragOperation)operation 36 | { 37 | if (NSDragOperationNone != operation) return; 38 | if ([self containsScreenPoint:screenPoint]) return; 39 | if (![self.dataSource respondsToSelector:@selector(tableView:performDropOutsideViewAtPoint:)]) return; 40 | if (![(id)self.dataSource tableView:self performDropOutsideViewAtPoint:screenPoint]) return; 41 | 42 | NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, screenPoint, NSZeroSize, nil, nil, nil); 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /ManOpen/PrefPanelController.h: -------------------------------------------------------------------------------- 1 | /* PrefPanelController.h created by lindberg on Fri 08-Oct-1999 */ 2 | 3 | #import "SystemType.h" 4 | #import 5 | 6 | @class NSMutableArray; 7 | @class NSFont, NSColor; 8 | @class NSArrayController; 9 | @class NSTableView, NSTextField, NSPopUpButton, NSMatrix; 10 | 11 | @interface PrefPanelController : NSWindowController 12 | { 13 | NSMutableArray *manPathArray; 14 | IBOutlet NSArrayController *manPathController; 15 | IBOutlet NSTableView *manPathTableView; 16 | IBOutlet NSTextField *fontField; 17 | IBOutlet NSMatrix *generalSwitchMatrix; 18 | IBOutlet NSPopUpButton *appPopup; 19 | } 20 | 21 | + (id)sharedInstance; 22 | + (void)registerManDefaults; 23 | 24 | - (IBAction)openFontPanel:(id)sender; 25 | 26 | @end 27 | 28 | @interface PrefPanelController (ManPath) 29 | - (IBAction)addPathFromPanel:(id)sender; 30 | @end 31 | @interface PrefPanelController (DefaultManApp) 32 | - (IBAction)chooseNewApp:(id)sender; 33 | @end 34 | -------------------------------------------------------------------------------- /ManOpen/SystemType.h: -------------------------------------------------------------------------------- 1 | /* 2 | * The FOUNDATION_STATIC_INLINE #define appeared in Rhapsody, so if it's 3 | * not there we're on OPENSTEP. 4 | */ 5 | #import 6 | #ifndef FOUNDATION_STATIC_INLINE 7 | #define OPENSTEP 8 | #else 9 | /* Cocoa (MacOS X) removed a bunch of defines from NSDebug.h */ 10 | #import 11 | #ifndef NSZoneMallocEvent 12 | #define MACOS_X 13 | #endif 14 | #endif 15 | 16 | #if !defined(MAC_OS_X_VERSION_10_5) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_5 17 | // compiling against 10.4 or before headers 18 | typedef int NSInteger; 19 | typedef unsigned NSUInteger; 20 | typedef float CGFloat; 21 | typedef NSUInteger NSStringCompareOptions; 22 | #endif 23 | 24 | #ifndef NSFoundationVersionNumber10_4 25 | #define NSFoundationVersionNumber10_4 567.0 26 | #endif 27 | #ifndef NSFoundationVersionNumber10_5 28 | #define NSFoundationVersionNumber10_5 677.00 29 | #endif 30 | #ifndef NSFoundationVersionNumber10_6 31 | #define NSFoundationVersionNumber10_6 751.00 32 | #endif 33 | #define IsLeopard() (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_4) 34 | #define IsSnowLeopard() (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_5) 35 | #define IsLion() (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_6) 36 | -------------------------------------------------------------------------------- /ManOpen/XManPageURLComponents.h: -------------------------------------------------------------------------------- 1 | // 2 | // XManPageURLComponents.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/3/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | @class ManPage; 12 | 13 | 14 | @interface XManPageURLComponents : NSObject 15 | 16 | @property (copy) NSString *aproposKeyword; 17 | @property (copy) NSArray *manPages; 18 | 19 | - (instancetype)initWithAproposKeyword:(NSString *)aproposKeyword; 20 | 21 | - (instancetype)initWithManPages:(NSArray *)manPages; 22 | 23 | - (instancetype)initWithURL:(NSURL *)url; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /ManOpen/XManPageURLComponents.m: -------------------------------------------------------------------------------- 1 | // 2 | // XManPageURLComponents.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/3/18. 6 | // 7 | 8 | #import "XManPageURLComponents.h" 9 | 10 | #import "ManPage.h" 11 | #import "NSURL+ManOpen.h" 12 | 13 | 14 | @implementation XManPageURLComponents 15 | 16 | - (instancetype)initWithAproposKeyword:(NSString *)aproposKeyword 17 | { 18 | self = [super init]; 19 | if (self) { 20 | _aproposKeyword = aproposKeyword.length ? [aproposKeyword copy] : nil; 21 | _manPages = nil; 22 | } 23 | return self; 24 | } 25 | 26 | - (instancetype)initWithManPages:(NSArray *)manPages 27 | { 28 | self = [super init]; 29 | if (self) { 30 | _manPages = manPages.count ? [manPages copy] : nil; 31 | } 32 | return self; 33 | } 34 | 35 | - (instancetype)initWithURL:(NSURL *)url 36 | { 37 | if (!url.isXManPageScheme) return nil; 38 | 39 | NSString *resourceSpecifier = url.resourceSpecifier; 40 | NSString *aproposSuffix = @";type=a"; 41 | BOOL isApropos = [resourceSpecifier hasSuffix:aproposSuffix]; 42 | if (isApropos) { 43 | NSUInteger index = resourceSpecifier.length - aproposSuffix.length; 44 | resourceSpecifier = [resourceSpecifier substringToIndex:index]; 45 | } 46 | 47 | NSPredicate *notRootPredicate = [NSPredicate predicateWithFormat:@"'/' != SELF"]; 48 | NSArray *pathComponents = [resourceSpecifier.pathComponents filteredArrayUsingPredicate:notRootPredicate]; 49 | if (!pathComponents.count) return nil; 50 | 51 | NSMutableArray *manPages = [[NSMutableArray new] autorelease]; 52 | if (1 == pathComponents.count) { 53 | ManPage *manPage = [[ManPage alloc] initWithName:pathComponents.firstObject]; 54 | [manPages addObject:manPage]; 55 | [manPage release]; 56 | } else { 57 | NSString *section = nil; 58 | for (NSString *pathComponent in pathComponents) { 59 | if (!section && [ManPage isSection:pathComponent]) { 60 | section = pathComponent; 61 | } else { 62 | ManPage *manPage = [[ManPage alloc] initWithSection:section 63 | andName:pathComponent]; 64 | [manPages addObject:manPage]; 65 | [manPage release]; 66 | section = nil; 67 | } 68 | } 69 | } 70 | 71 | if (isApropos) { 72 | return [self initWithAproposKeyword:manPages.firstObject.name]; 73 | } else { 74 | return [self initWithManPages:manPages]; 75 | } 76 | } 77 | 78 | - (void)dealloc 79 | { 80 | [_aproposKeyword release]; 81 | [_manPages release]; 82 | [super dealloc]; 83 | } 84 | 85 | @end 86 | -------------------------------------------------------------------------------- /ManOpen/en.lproj/Apropos.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ManOpen/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\mac\ansicpg10000\cocoartf102 2 | {\fonttbl\f0\fswiss\fcharset77 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | \margl120\margr120\vieww11520\viewh8400\viewkind0 5 | \pard\tx520\tx1060\tx1600\tx2120\tx2660\tx3200\tx3720\tx4260\tx4800\tx5320\qc 6 | 7 | \f0\fs24 \cf0 \ 8 | \ 9 | A graphical viewer for Unix manual pages\ 10 | \ 11 | Carl Lindberg lindberg@clindberg.org\ 12 | \ 13 | http://www.clindberg.org\ 14 | } -------------------------------------------------------------------------------- /ManOpen/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localizations for the type names so that RCDefaultApp shows them well */ 2 | man = "Man Page"; 3 | cat = "Preformatted Man Page"; 4 | mangz = "Compressed Man Page"; 5 | catgz = "Compressed Preformatted Man Page"; 6 | 7 | -------------------------------------------------------------------------------- /ManOpen/en.lproj/ManPage.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /ManOpen/helpQMark.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/ManOpen/helpQMark.tiff -------------------------------------------------------------------------------- /ManOpenTests/FileURLComponentsTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // FileURLComponentsTests.m 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 1/14/18. 6 | // 7 | 8 | #import 9 | #import "FileURLComponents.h" 10 | 11 | 12 | @interface FileURLComponentsTests : XCTestCase 13 | @end 14 | 15 | 16 | @implementation FileURLComponentsTests 17 | 18 | - (void)testInit 19 | { 20 | FileURLComponents *components = [[FileURLComponents new] autorelease]; 21 | 22 | XCTAssertFalse(components.isAbsolute); 23 | XCTAssertFalse(components.isDirectory); 24 | XCTAssertTrue(components.isLocalhost); 25 | XCTAssertNil(components.host); 26 | XCTAssertNil(components.path); 27 | } 28 | 29 | #pragma mark - initWithHost:andPath: 30 | 31 | - (void)testInitWithHostAndPath 32 | { 33 | FileURLComponents *components = [[FileURLComponents alloc] initWithHost:nil 34 | andPath:@"/usr/bin"]; 35 | [components autorelease]; 36 | 37 | XCTAssertTrue(components.isAbsolute); 38 | XCTAssertFalse(components.isDirectory); 39 | XCTAssertTrue(components.isLocalhost); 40 | XCTAssertNil(components.host); 41 | XCTAssertEqualObjects(@"/usr/bin", components.path); 42 | } 43 | 44 | - (void)testInitWithHostAndPath_with_empty_host 45 | { 46 | FileURLComponents *components = [[FileURLComponents alloc] initWithHost:@"" 47 | andPath:@"/usr/bin"]; 48 | [components autorelease]; 49 | 50 | XCTAssertTrue(components.isAbsolute); 51 | XCTAssertFalse(components.isDirectory); 52 | XCTAssertTrue(components.isLocalhost); 53 | XCTAssertNil(components.host); 54 | XCTAssertEqualObjects(@"/usr/bin", components.path); 55 | } 56 | 57 | - (void)testInitWithHostAndPath_with_host 58 | { 59 | FileURLComponents *components = [[FileURLComponents alloc] initWithHost:@"example.com" 60 | andPath:@"/usr/bin"]; 61 | [components autorelease]; 62 | 63 | XCTAssertTrue(components.isAbsolute); 64 | XCTAssertFalse(components.isDirectory); 65 | XCTAssertFalse(components.isLocalhost); 66 | XCTAssertEqualObjects(@"example.com", components.host); 67 | XCTAssertEqualObjects(@"/usr/bin", components.path); 68 | } 69 | 70 | - (void)testInitWithHostAndPath_with_localhost 71 | { 72 | FileURLComponents *components = [[FileURLComponents alloc] initWithHost:@"localhost" 73 | andPath:@"/usr/bin"]; 74 | [components autorelease]; 75 | 76 | XCTAssertTrue(components.isAbsolute); 77 | XCTAssertFalse(components.isDirectory); 78 | XCTAssertTrue(components.isLocalhost); 79 | XCTAssertEqualObjects(@"localhost", components.host); 80 | XCTAssertEqualObjects(@"/usr/bin", components.path); 81 | } 82 | 83 | - (void)testInitWithHostAndPath_with_LOCALHOST 84 | { 85 | FileURLComponents *components = [[FileURLComponents alloc] initWithHost:@"LOCALHOST" 86 | andPath:@"/usr/bin"]; 87 | [components autorelease]; 88 | 89 | XCTAssertTrue(components.isAbsolute); 90 | XCTAssertFalse(components.isDirectory); 91 | XCTAssertTrue(components.isLocalhost); 92 | XCTAssertEqualObjects(@"LOCALHOST", components.host); 93 | XCTAssertEqualObjects(@"/usr/bin", components.path); 94 | } 95 | 96 | - (void)testInitWithHostAndPath_with_relative_path 97 | { 98 | FileURLComponents *components = [[FileURLComponents alloc] initWithHost:nil 99 | andPath:@"share/man/man1"]; 100 | [components autorelease]; 101 | 102 | XCTAssertFalse(components.isAbsolute); 103 | XCTAssertFalse(components.isDirectory); 104 | XCTAssertTrue(components.isLocalhost); 105 | XCTAssertNil(components.host); 106 | XCTAssertEqualObjects(@"share/man/man1", components.path); 107 | } 108 | 109 | - (void)testInitWithHostAndPath_with_directory_path 110 | { 111 | FileURLComponents *components = [[FileURLComponents alloc] initWithHost:nil 112 | andPath:@"/usr/local/bin/"]; 113 | [components autorelease]; 114 | 115 | XCTAssertTrue(components.isAbsolute); 116 | XCTAssertTrue(components.isDirectory); 117 | XCTAssertTrue(components.isLocalhost); 118 | XCTAssertNil(components.host); 119 | XCTAssertEqualObjects(@"/usr/local/bin/", components.path); 120 | } 121 | 122 | #pragma mark - initWithURL: 123 | 124 | - (void)testInitWithURL_with_empty_host 125 | { 126 | NSURL *url = [NSURL URLWithString:@"file:///usr/share/man/man1"]; 127 | 128 | FileURLComponents *components = [[FileURLComponents alloc] initWithURL:url]; 129 | [components autorelease]; 130 | 131 | XCTAssertTrue(components.isAbsolute); 132 | XCTAssertFalse(components.isDirectory); 133 | XCTAssertTrue(components.isLocalhost); 134 | XCTAssertNil(components.host); 135 | XCTAssertEqualObjects(@"/usr/share/man/man1", components.path); 136 | } 137 | 138 | - (void)testInitWithURL_with_host 139 | { 140 | NSURL *url = [NSURL URLWithString:@"file://example.com/usr/share/man/man1"]; 141 | 142 | FileURLComponents *components = [[FileURLComponents alloc] initWithURL:url]; 143 | [components autorelease]; 144 | 145 | XCTAssertTrue(components.isAbsolute); 146 | XCTAssertFalse(components.isDirectory); 147 | XCTAssertFalse(components.isLocalhost); 148 | XCTAssertEqualObjects(@"example.com", components.host); 149 | XCTAssertEqualObjects(@"/usr/share/man/man1", components.path); 150 | } 151 | 152 | - (void)testInitWithURL_with_localhost 153 | { 154 | NSURL *url = [NSURL URLWithString:@"file://localhost/usr/share/man/man1"]; 155 | 156 | FileURLComponents *components = [[FileURLComponents alloc] initWithURL:url]; 157 | [components autorelease]; 158 | 159 | XCTAssertTrue(components.isAbsolute); 160 | XCTAssertFalse(components.isDirectory); 161 | XCTAssertTrue(components.isLocalhost); 162 | XCTAssertEqualObjects(@"localhost", components.host); 163 | XCTAssertEqualObjects(@"/usr/share/man/man1", components.path); 164 | } 165 | 166 | - (void)testInitWithURL_without_host 167 | { 168 | NSURL *url = [NSURL URLWithString:@"file:/usr/share/man/man1"]; 169 | 170 | FileURLComponents *components = [[FileURLComponents alloc] initWithURL:url]; 171 | [components autorelease]; 172 | 173 | XCTAssertTrue(components.isAbsolute); 174 | XCTAssertFalse(components.isDirectory); 175 | XCTAssertTrue(components.isLocalhost); 176 | XCTAssertNil(components.host); 177 | XCTAssertEqualObjects(@"/usr/share/man/man1", components.path); 178 | } 179 | 180 | - (void)testInitWithURL_without_path 181 | { 182 | NSURL *url = [NSURL URLWithString:@"file://example.com"]; 183 | 184 | FileURLComponents *components = [[FileURLComponents alloc] initWithURL:url]; 185 | [components autorelease]; 186 | 187 | XCTAssertFalse(components.isAbsolute); 188 | XCTAssertFalse(components.isDirectory); 189 | XCTAssertFalse(components.isLocalhost); 190 | XCTAssertEqualObjects(@"example.com", components.host); 191 | XCTAssertNil(components.path); 192 | } 193 | 194 | @end 195 | -------------------------------------------------------------------------------- /ManOpenTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ManOpenTests/ManDocumentControllerTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // ManDocumentControllerTests.m 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 2/10/18. 6 | // 7 | 8 | #import 9 | #import "ManDocumentController.h" 10 | 11 | 12 | @interface ManDocumentControllerTests : XCTestCase 13 | 14 | @property (retain) ManDocumentController *controller; 15 | 16 | @end 17 | 18 | 19 | @implementation ManDocumentControllerTests 20 | 21 | #pragma mark - manCommandWithManPath 22 | 23 | - (void)testManCommandWithManPath 24 | { 25 | XCTAssertEqualObjects(@"/usr/bin/man", 26 | [_controller manCommandWithManPath:nil]); 27 | XCTAssertEqualObjects(@"/usr/bin/man", 28 | [_controller manCommandWithManPath:@""]); 29 | XCTAssertEqualObjects(@"/usr/bin/man -M '/usr/man'", 30 | [_controller manCommandWithManPath:@"/usr/man"]); 31 | XCTAssertEqualObjects(@"/usr/bin/man -M '/usr/man:/it'\\''s great'", 32 | [_controller manCommandWithManPath:@"/usr/man:/it's great"]); 33 | } 34 | 35 | #pragma mark - setUp 36 | 37 | - (void)setUp 38 | { 39 | [super setUp]; 40 | _controller = [[ManDocumentController sharedDocumentController] retain]; 41 | } 42 | 43 | - (void)tearDown 44 | { 45 | [super tearDown]; 46 | [_controller release]; 47 | } 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /ManOpenTests/ManOpenURLHandlerCommandTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // ManOpenURLHandlerCommandTests.m 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 11/9/17. 6 | // 7 | 8 | #import 9 | #import "ManOpenURLHandlerCommand.h" 10 | #import "MockManDocumentController.h" 11 | 12 | 13 | @interface ManOpenURLHandlerCommandTests : XCTestCase 14 | 15 | @property (retain) ManOpenURLHandlerCommand *command; 16 | @property (retain) MockManDocumentController *mockManDocumentController; 17 | 18 | @end 19 | 20 | 21 | @implementation ManOpenURLHandlerCommandTests 22 | 23 | #pragma mark - x-man-page: scheme 24 | 25 | - (void)test_x_man_page_scheme 26 | { 27 | _command.directParameter = @"x-man-page://grep"; 28 | 29 | [_command performDefaultImplementation]; 30 | 31 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_was_called); 32 | XCTAssertFalse(_mockManDocumentController.openFile_was_called); 33 | 34 | XCTAssertEqual(1, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 35 | NSArray *firstCall = _mockManDocumentController.openNameSectionManPathForceToFront_calls.firstObject; 36 | XCTAssertEqualObjects(@"grep", [firstCall objectAtIndex:0]); 37 | XCTAssertEqualObjects([NSNull null], [firstCall objectAtIndex:1]); 38 | XCTAssertEqualObjects([NSNull null], [firstCall objectAtIndex:2]); 39 | XCTAssertEqualObjects(@(YES), [firstCall objectAtIndex:3]); 40 | } 41 | 42 | - (void)test_x_man_page_scheme_with_multiple_pages 43 | { 44 | _command.directParameter = @"x-man-page://1/grep/printf/2/open"; 45 | 46 | [_command performDefaultImplementation]; 47 | 48 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_was_called); 49 | XCTAssertFalse(_mockManDocumentController.openFile_was_called); 50 | 51 | XCTAssertEqual(3, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 52 | 53 | NSArray *firstCall = _mockManDocumentController.openNameSectionManPathForceToFront_calls.firstObject; 54 | XCTAssertEqualObjects(@"grep", [firstCall objectAtIndex:0]); 55 | XCTAssertEqualObjects(@"1", [firstCall objectAtIndex:1]); 56 | XCTAssertEqualObjects([NSNull null], [firstCall objectAtIndex:2]); 57 | XCTAssertEqualObjects(@(YES), [firstCall objectAtIndex:3]); 58 | 59 | NSArray *secondCall = [_mockManDocumentController.openNameSectionManPathForceToFront_calls objectAtIndex:1]; 60 | XCTAssertEqualObjects(@"printf", [secondCall objectAtIndex:0]); 61 | XCTAssertEqualObjects([NSNull null], [secondCall objectAtIndex:1]); 62 | XCTAssertEqualObjects([NSNull null], [secondCall objectAtIndex:2]); 63 | XCTAssertEqualObjects(@(YES), [secondCall objectAtIndex:3]); 64 | 65 | NSArray *thirdCall = [_mockManDocumentController.openNameSectionManPathForceToFront_calls objectAtIndex:2]; 66 | XCTAssertEqualObjects(@"open", [thirdCall objectAtIndex:0]); 67 | XCTAssertEqualObjects(@"2", [thirdCall objectAtIndex:1]); 68 | XCTAssertEqualObjects([NSNull null], [thirdCall objectAtIndex:2]); 69 | XCTAssertEqualObjects(@(YES), [thirdCall objectAtIndex:3]); 70 | } 71 | 72 | - (void)test_x_man_page_scheme_with_apropos_keyword 73 | { 74 | _command.directParameter = @"x-man-page://print;type=a"; 75 | 76 | [_command performDefaultImplementation]; 77 | 78 | XCTAssertFalse(_mockManDocumentController.openFile_was_called); 79 | XCTAssertEqual(0, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 80 | 81 | XCTAssertTrue(_mockManDocumentController.openAproposManPathForceToFront_was_called); 82 | XCTAssertEqualObjects(@"print", _mockManDocumentController.openAproposManPathForceToFront_apropos); 83 | XCTAssertNil(_mockManDocumentController.openAproposManPathForceToFront_manPath); 84 | XCTAssertTrue(_mockManDocumentController.openAproposManPathForceToFront_force); 85 | } 86 | 87 | #pragma mark - file: scheme 88 | 89 | - (void)test_file_scheme 90 | { 91 | _command.directParameter = @"file:///usr/share/man/man1/basename.1"; 92 | 93 | [_command performDefaultImplementation]; 94 | 95 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_was_called); 96 | XCTAssertEqual(0, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 97 | 98 | XCTAssertTrue(_mockManDocumentController.openFile_was_called); 99 | XCTAssertEqualObjects(@"/usr/share/man/man1/basename.1", 100 | _mockManDocumentController.openFile_filename); 101 | XCTAssertTrue(_mockManDocumentController.openFile_force); 102 | } 103 | 104 | - (void)test_file_scheme_with_remote_host 105 | { 106 | _command.directParameter = @"file://www.example.com/usr/share/man/man6/banner.6"; 107 | 108 | [_command performDefaultImplementation]; 109 | 110 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_was_called); 111 | XCTAssertFalse(_mockManDocumentController.openFile_was_called); 112 | XCTAssertEqual(0, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 113 | } 114 | 115 | - (void)test_file_scheme_with_relative_path 116 | { 117 | _command.directParameter = @"file:usr/share/man/man8/arp.8"; 118 | 119 | [_command performDefaultImplementation]; 120 | 121 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_was_called); 122 | XCTAssertFalse(_mockManDocumentController.openFile_was_called); 123 | XCTAssertEqual(0, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 124 | } 125 | 126 | #pragma mark - manopen: scheme 127 | 128 | - (void)test_manopen_scheme_with_man_page 129 | { 130 | _command.directParameter = @"manopen://3/printf"; 131 | 132 | [_command performDefaultImplementation]; 133 | 134 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_was_called); 135 | XCTAssertFalse(_mockManDocumentController.openFile_was_called); 136 | 137 | XCTAssertEqual(1, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 138 | NSArray *firstCall = _mockManDocumentController.openNameSectionManPathForceToFront_calls.firstObject; 139 | XCTAssertEqualObjects(@"printf", [firstCall objectAtIndex:0]); 140 | XCTAssertEqualObjects(@"3", [firstCall objectAtIndex:1]); 141 | XCTAssertEqualObjects([NSNull null], [firstCall objectAtIndex:2]); 142 | XCTAssertEqualObjects(@(YES), [firstCall objectAtIndex:3]); 143 | } 144 | 145 | - (void)test_manopen_scheme_with_man_page_with_query_parameters 146 | { 147 | _command.directParameter = @"manopen:///mkfifo?background=true&MANPATH=/usr/share:/usr/man"; 148 | 149 | [_command performDefaultImplementation]; 150 | 151 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_was_called); 152 | XCTAssertFalse(_mockManDocumentController.openFile_was_called); 153 | 154 | XCTAssertEqual(1, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 155 | NSArray *firstCall = _mockManDocumentController.openNameSectionManPathForceToFront_calls.firstObject; 156 | XCTAssertEqualObjects(@"mkfifo", [firstCall objectAtIndex:0]); 157 | XCTAssertEqualObjects([NSNull null], [firstCall objectAtIndex:1]); 158 | XCTAssertEqualObjects(@"/usr/share:/usr/man", [firstCall objectAtIndex:2]); 159 | XCTAssertEqualObjects(@(NO), [firstCall objectAtIndex:3]); 160 | } 161 | 162 | - (void)test_manopen_scheme_with_apropos_search 163 | { 164 | _command.directParameter = @"manopen://apropos/edit"; 165 | 166 | [_command performDefaultImplementation]; 167 | 168 | XCTAssertFalse(_mockManDocumentController.openFile_was_called); 169 | XCTAssertEqual(0, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 170 | 171 | XCTAssertTrue(_mockManDocumentController.openAproposManPathForceToFront_was_called); 172 | XCTAssertEqualObjects(@"edit", _mockManDocumentController.openAproposManPathForceToFront_apropos); 173 | XCTAssertNil(_mockManDocumentController.openAproposManPathForceToFront_manPath); 174 | XCTAssertTrue(_mockManDocumentController.openAproposManPathForceToFront_force); 175 | } 176 | 177 | - (void)test_manopen_scheme_with_apropos_search_with_query_parameters 178 | { 179 | _command.directParameter = @"manopen://apropos/route?background=true&MANPATH=/usr/share:/usr/man"; 180 | 181 | [_command performDefaultImplementation]; 182 | 183 | XCTAssertFalse(_mockManDocumentController.openFile_was_called); 184 | XCTAssertEqual(0, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 185 | 186 | XCTAssertTrue(_mockManDocumentController.openAproposManPathForceToFront_was_called); 187 | XCTAssertEqualObjects(@"route", _mockManDocumentController.openAproposManPathForceToFront_apropos); 188 | XCTAssertEqualObjects(@"/usr/share:/usr/man", _mockManDocumentController.openAproposManPathForceToFront_manPath); 189 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_force); 190 | } 191 | 192 | - (void)test_manopen_scheme_with_file_path 193 | { 194 | _command.directParameter = @"manopen:/usr/share/man/man1/wget.1"; 195 | 196 | [_command performDefaultImplementation]; 197 | 198 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_was_called); 199 | XCTAssertEqual(0, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 200 | 201 | XCTAssertTrue(_mockManDocumentController.openFile_was_called); 202 | XCTAssertEqualObjects(@"/usr/share/man/man1/wget.1", 203 | _mockManDocumentController.openFile_filename); 204 | XCTAssertTrue(_mockManDocumentController.openFile_force); 205 | } 206 | 207 | - (void)test_manopen_scheme_with_file_path_in_background 208 | { 209 | _command.directParameter = @"manopen:/usr/share/man/man8/nginx.8?background=true"; 210 | 211 | [_command performDefaultImplementation]; 212 | 213 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_was_called); 214 | XCTAssertEqual(0, _mockManDocumentController.openNameSectionManPathForceToFront_calls.count); 215 | 216 | XCTAssertTrue(_mockManDocumentController.openFile_was_called); 217 | XCTAssertEqualObjects(@"/usr/share/man/man8/nginx.8", 218 | _mockManDocumentController.openFile_filename); 219 | XCTAssertFalse(_mockManDocumentController.openFile_force); 220 | } 221 | 222 | #pragma mark - unsupported scheme 223 | 224 | - (void)test_unsupported_scheme 225 | { 226 | _command.directParameter = @"http://www.example.com/index.html"; 227 | 228 | [_command performDefaultImplementation]; 229 | 230 | XCTAssertFalse(_mockManDocumentController.openAproposManPathForceToFront_was_called); 231 | XCTAssertFalse(_mockManDocumentController.openFile_was_called); 232 | } 233 | 234 | #pragma mark - 235 | 236 | - (void)setUp 237 | { 238 | [super setUp]; 239 | _mockManDocumentController = [MockManDocumentController new]; 240 | _command = [ManOpenURLHandlerCommand new]; 241 | _command.manDocumentController = (ManDocumentController *)_mockManDocumentController; 242 | } 243 | 244 | - (void)tearDown 245 | { 246 | [super tearDown]; 247 | [_command release]; 248 | [_mockManDocumentController release]; 249 | } 250 | 251 | @end 252 | -------------------------------------------------------------------------------- /ManOpenTests/ManPageTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // ManPageTests.m 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 1/20/18. 6 | // 7 | 8 | #import 9 | #import "ManPage.h" 10 | 11 | 12 | @interface ManPageTests : XCTestCase 13 | @end 14 | 15 | 16 | @implementation ManPageTests 17 | 18 | #pragma mark - +isSection 19 | 20 | - (void)testIsSection 21 | { 22 | XCTAssertTrue([ManPage isSection:@"1"]); 23 | XCTAssertTrue([ManPage isSection:@"3pm"]); 24 | XCTAssertTrue([ManPage isSection:@"n"]); 25 | XCTAssertTrue([ManPage isSection:@"x"]); 26 | 27 | XCTAssertFalse([ManPage isSection:nil]); 28 | XCTAssertFalse([ManPage isSection:@""]); 29 | XCTAssertFalse([ManPage isSection:@"a1"]); 30 | XCTAssertFalse([ManPage isSection:@"foobar"]); 31 | } 32 | 33 | #pragma mark - -init 34 | 35 | - (void)testInit 36 | { 37 | ManPage *manPage = [[ManPage new] autorelease]; 38 | 39 | XCTAssertNil(manPage.section); 40 | XCTAssertNil(manPage.name); 41 | XCTAssertEqualObjects(@"", manPage.description); 42 | } 43 | 44 | #pragma mark - -initWithSection:andName: 45 | 46 | - (void)testInitWithSectionAndName 47 | { 48 | ManPage *manPage = [[ManPage alloc] initWithSection:@"1" 49 | andName:@"intro"]; 50 | [manPage autorelease]; 51 | 52 | XCTAssertEqualObjects(@"1", manPage.section); 53 | XCTAssertEqualObjects(@"intro", manPage.name); 54 | XCTAssertEqualObjects(@"intro(1)", manPage.description); 55 | } 56 | 57 | - (void)testInitWithSectionAndName_with_long_section 58 | { 59 | ManPage *manPage = [[ManPage alloc] initWithSection:@"3p" 60 | andName:@"open"]; 61 | [manPage autorelease]; 62 | 63 | XCTAssertEqualObjects(@"3p", manPage.section); 64 | XCTAssertEqualObjects(@"open", manPage.name); 65 | XCTAssertEqualObjects(@"open(3p)", manPage.description); 66 | } 67 | 68 | - (void)testInitWithSectionAndName_with_empty_section 69 | { 70 | ManPage *manPage = [[ManPage alloc] initWithSection:@"" 71 | andName:@"grep"]; 72 | [manPage autorelease]; 73 | 74 | XCTAssertNil(manPage.section); 75 | XCTAssertEqualObjects(@"grep", manPage.name); 76 | XCTAssertEqualObjects(@"grep", manPage.description); 77 | } 78 | 79 | - (void)testInitWithSectionAndName_with_invalid_section 80 | { 81 | ManPage *manPage = [[ManPage alloc] initWithSection:@"foobar" 82 | andName:@"grep"]; 83 | [manPage autorelease]; 84 | 85 | XCTAssertNil(manPage.section); 86 | XCTAssertEqualObjects(@"grep", manPage.name); 87 | XCTAssertEqualObjects(@"grep", manPage.description); 88 | } 89 | 90 | - (void)testInitWithSectionAndName_with_empty_section_and_name 91 | { 92 | ManPage *manPage = [[ManPage alloc] initWithSection:@"" 93 | andName:@""]; 94 | [manPage autorelease]; 95 | 96 | XCTAssertNil(manPage.section); 97 | XCTAssertNil(manPage.name); 98 | XCTAssertEqualObjects(@"", manPage.description); 99 | } 100 | 101 | #pragma mark - -initWithName: 102 | 103 | - (void)testInitWithName 104 | { 105 | ManPage *manPage = [[ManPage alloc] initWithName:@"find"]; 106 | [manPage autorelease]; 107 | 108 | XCTAssertNil(manPage.section); 109 | XCTAssertEqualObjects(@"find", manPage.name); 110 | XCTAssertEqualObjects(@"find", manPage.description); 111 | } 112 | 113 | - (void)testInitWithName_with_empty_name 114 | { 115 | ManPage *manPage = [[ManPage alloc] initWithName:@""]; 116 | [manPage autorelease]; 117 | 118 | XCTAssertNil(manPage.section); 119 | XCTAssertNil(manPage.name); 120 | XCTAssertEqualObjects(@"", manPage.description); 121 | } 122 | 123 | @end 124 | -------------------------------------------------------------------------------- /ManOpenTests/MockManDocumentController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MockManDocumentController.h 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 11/11/17. 6 | // 7 | 8 | #import 9 | 10 | 11 | @interface MockManDocumentController : NSObject 12 | 13 | @property BOOL openAproposManPathForceToFront_was_called; 14 | @property (copy) NSString *openAproposManPathForceToFront_apropos; 15 | @property (copy) NSString *openAproposManPathForceToFront_manPath; 16 | @property BOOL openAproposManPathForceToFront_force; 17 | 18 | @property BOOL openFile_was_called; 19 | @property (copy) NSString *openFile_filename; 20 | @property BOOL openFile_force; 21 | 22 | @property (retain) NSMutableArray *openNameSectionManPathForceToFront_calls; 23 | 24 | - (void)openApropos:(NSString *)apropos 25 | manPath:(NSString *)manPath 26 | forceToFront:(BOOL)force; 27 | 28 | - (void)openFile:(NSString *)filename 29 | forceToFront:(BOOL)force; 30 | 31 | - (void)openName:(NSString *)name 32 | section:(NSString *)section 33 | manPath:(NSString *)manPath 34 | forceToFront:(BOOL)force; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /ManOpenTests/MockManDocumentController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MockManDocumentController.m 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 11/11/17. 6 | // 7 | 8 | #import "MockManDocumentController.h" 9 | 10 | 11 | @implementation MockManDocumentController 12 | 13 | - (instancetype)init 14 | { 15 | self = [super init]; 16 | if (self) { 17 | _openNameSectionManPathForceToFront_calls = [NSMutableArray new]; 18 | } 19 | return self; 20 | } 21 | 22 | - (void)dealloc 23 | { 24 | [_openAproposManPathForceToFront_apropos release]; 25 | [_openAproposManPathForceToFront_manPath release]; 26 | [_openFile_filename release]; 27 | [_openNameSectionManPathForceToFront_calls release]; 28 | [super dealloc]; 29 | } 30 | 31 | - (void)openApropos:(NSString *)apropos 32 | manPath:(NSString *)manPath 33 | forceToFront:(BOOL)force 34 | { 35 | _openAproposManPathForceToFront_was_called = YES; 36 | 37 | [_openAproposManPathForceToFront_apropos release]; 38 | _openAproposManPathForceToFront_apropos = [apropos copy]; 39 | 40 | [_openAproposManPathForceToFront_manPath release]; 41 | _openAproposManPathForceToFront_manPath = [manPath copy]; 42 | 43 | _openAproposManPathForceToFront_force = force; 44 | } 45 | 46 | - (void)openFile:(NSString *)filename 47 | forceToFront:(BOOL)force 48 | { 49 | _openFile_was_called = YES; 50 | [_openFile_filename release]; 51 | _openFile_filename = [filename copy]; 52 | _openFile_force = force; 53 | } 54 | 55 | - (void)openName:(NSString *)name 56 | section:(NSString *)section 57 | manPath:(NSString *)manPath 58 | forceToFront:(BOOL)force 59 | { 60 | NSArray *arguments = @[ 61 | name ?: [NSNull null], 62 | section ?: [NSNull null], 63 | manPath ?: [NSNull null], 64 | @(force) 65 | ]; 66 | [_openNameSectionManPathForceToFront_calls addObject:arguments]; 67 | } 68 | 69 | @end 70 | -------------------------------------------------------------------------------- /ManOpenTests/NSDictionary+ManOpenTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+ManOpenTests.m 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 12/25/17. 6 | // 7 | 8 | #import 9 | #import "NSDictionary+ManOpen.h" 10 | 11 | 12 | @interface NSDictionary_ManOpenTests : XCTestCase 13 | @end 14 | 15 | 16 | @implementation NSDictionary_ManOpenTests 17 | 18 | #pragma mark - dictionaryWithURLQuery: 19 | 20 | - (void)testDictionaryWithURLQuery_for_nil 21 | { 22 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:nil]; 23 | XCTAssertNil(dictionary); 24 | } 25 | 26 | - (void)testDictionaryWithURLQuery_for_empty_string 27 | { 28 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@""]; 29 | XCTAssertEqualObjects(@{}, dictionary); 30 | } 31 | 32 | - (void)testDictionaryWithURLQuery_for_key_only 33 | { 34 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"foo"]; 35 | XCTAssertEqualObjects(@{ @"foo": @"" }, dictionary); 36 | } 37 | 38 | - (void)testDictionaryWithURLQuery_for_key_and_equals_only 39 | { 40 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"foo="]; 41 | XCTAssertEqualObjects(@{ @"foo": @"" }, dictionary); 42 | } 43 | 44 | - (void)testDictionaryWithURLQuery_for_value_only 45 | { 46 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"=bar"]; 47 | XCTAssertEqualObjects(@{ @"": @"bar" }, dictionary); 48 | } 49 | 50 | - (void)testDictionaryWithURLQuery_for_equals_only 51 | { 52 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"="]; 53 | XCTAssertEqualObjects(@{ @"": @"" }, dictionary); 54 | } 55 | 56 | - (void)testDictionaryWithURLQuery_for_one_key 57 | { 58 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"foo=bar"]; 59 | XCTAssertEqualObjects(@{ @"foo": @"bar" }, dictionary); 60 | } 61 | 62 | - (void)testDictionaryWithURLQuery_for_url_encoded_key_and_value 63 | { 64 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"2%2b2%3D5=odds%20%26%20ends"]; 65 | XCTAssertEqualObjects(@{ @"2+2=5": @"odds & ends" }, dictionary); 66 | } 67 | 68 | - (void)testDictionaryWithURLQuery_for_invalid_url_encoded_key_and_value 69 | { 70 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"%foo=%bar"]; 71 | XCTAssertEqualObjects(@{ @"%foo": @"%bar" }, dictionary); 72 | } 73 | 74 | - (void)testDictionaryWithURLQuery_for_two_keys 75 | { 76 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"foo=bar&color=red"]; 77 | NSDictionary *expected = @{ 78 | @"foo": @"bar", 79 | @"color": @"red", 80 | }; 81 | XCTAssertEqualObjects(expected, dictionary); 82 | } 83 | 84 | - (void)testDictionaryWithURLQuery_for_duplicate_key 85 | { 86 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"foo=bar&foo=BAR"]; 87 | XCTAssertEqualObjects(@{ @"foo": @"BAR" }, dictionary); 88 | } 89 | 90 | - (void)testDictionaryWithURLQuery_for_two_keys_with_whitespace 91 | { 92 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"\tfoo =%20bar &\tcolor%20= red\n"]; 93 | NSDictionary *expected = @{ @"foo": @"bar", @"color": @"red" }; 94 | XCTAssertEqualObjects(expected, dictionary); 95 | } 96 | 97 | - (void)testDictionaryWithURLQuery_with_internal_whitespace 98 | { 99 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"status=foo\tbar&color=red%0agreen&cool flavor=mint"]; 100 | NSDictionary *expected = @{ @"status": @"foo\tbar", @"color": @"red\ngreen", @"cool flavor": @"mint" }; 101 | XCTAssertEqualObjects(expected, dictionary); 102 | } 103 | 104 | - (void)testDictionaryWithURLQuery_for_manopen_scheme 105 | { 106 | NSDictionary *dictionary = [NSDictionary dictionaryWithURLQuery:@"background=true&MANPATH=/usr/share/man%3a/usr/local/share/man"]; 107 | NSDictionary *expected = @{ @"background": @"true", @"MANPATH": @"/usr/share/man:/usr/local/share/man" }; 108 | XCTAssertEqualObjects(expected, dictionary); 109 | } 110 | 111 | #pragma mark - urlQuery 112 | 113 | - (void)testURLQuery_for_empty 114 | { 115 | NSDictionary *dictionary = @{}; 116 | XCTAssertEqualObjects(@"", dictionary.urlQuery); 117 | } 118 | 119 | - (void)testURLQuery_for_one_parameter 120 | { 121 | NSDictionary *dictionary = @{ 122 | @"one": @"1", 123 | }; 124 | XCTAssertEqualObjects(@"?one=1", dictionary.urlQuery); 125 | } 126 | 127 | - (void)testURLQuery_for_multiple_parameters 128 | { 129 | NSDictionary *dictionary = @{ 130 | @"one": @"1", 131 | @"two": @"2", 132 | @"three": @"3", 133 | }; 134 | XCTAssertEqualObjects(@"?one=1&three=3&two=2", dictionary.urlQuery); 135 | } 136 | 137 | - (void)testURLQuery_for_multiple_parameters_needing_url_encoding 138 | { 139 | NSDictionary *dictionary = @{ 140 | @"one": @"1", 141 | @"two": @"1&1", 142 | @"one&two": @"1 and 2", 143 | }; 144 | XCTAssertEqualObjects(@"?one=1&one%26two=1%20and%202&two=1%261", dictionary.urlQuery); 145 | } 146 | 147 | - (void)testURLQuery_for_manopen_scheme_parameters 148 | { 149 | NSDictionary *dictionary = @{ 150 | @"MANPATH": @"/usr/share", 151 | @"background": @"true", 152 | }; 153 | XCTAssertEqualObjects(@"?MANPATH=%2Fusr%2Fshare&background=true", dictionary.urlQuery); 154 | } 155 | 156 | @end 157 | -------------------------------------------------------------------------------- /ManOpenTests/NSString+ManOpenTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+ManOpenTests.m 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 2/10/18. 6 | // 7 | 8 | #import 9 | #import "NSString+ManOpen.h" 10 | 11 | 12 | @interface NSString_ManOpenTests : XCTestCase 13 | @end 14 | 15 | 16 | @implementation NSString_ManOpenTests 17 | 18 | - (void)testManPageWords 19 | { 20 | NSArray *expected = @[]; 21 | XCTAssertEqualObjects(expected, @"".manPageWords); 22 | 23 | expected = @[ @"grep" ]; 24 | XCTAssertEqualObjects(expected, @"grep".manPageWords); 25 | 26 | expected = @[ @"printf(3)" ]; 27 | XCTAssertEqualObjects(expected, @"printf(3)".manPageWords); 28 | 29 | expected = @[ @"sed(1)" ]; 30 | XCTAssertEqualObjects(expected, @"sed (1)".manPageWords); 31 | 32 | expected = @[ @"more" ]; 33 | XCTAssertEqualObjects(expected, @"more,".manPageWords); 34 | 35 | expected = @[ @"more", @"less" ]; 36 | XCTAssertEqualObjects(expected, @"more, less".manPageWords); 37 | 38 | expected = @[ @"sed(1)", @"awk(1)" ]; 39 | XCTAssertEqualObjects(expected, @"sed (1) awk (1)".manPageWords); 40 | 41 | expected = @[ @"grep", @"printf(3)", @"logname", @"sed(1)", @"more" ]; 42 | XCTAssertEqualObjects(expected, @"grep, printf (3) log- name sed(1) more,".manPageWords); 43 | } 44 | 45 | - (void)testSingleQuotedShellWord 46 | { 47 | XCTAssertEqualObjects(@"''", @"".singleQuotedShellWord); 48 | XCTAssertEqualObjects(@"''\\'''", @"'".singleQuotedShellWord); 49 | XCTAssertEqualObjects(@"'/foo/bar'", @"/foo/bar".singleQuotedShellWord); 50 | XCTAssertEqualObjects(@"'/foo bar/baz'", @"/foo bar/baz".singleQuotedShellWord); 51 | XCTAssertEqualObjects(@"'/Apple'\\''s Stuff'", @"/Apple's Stuff".singleQuotedShellWord); 52 | XCTAssertEqualObjects(@"'/foo/'\\''bar'\\'''", @"/foo/'bar'".singleQuotedShellWord); 53 | XCTAssertEqualObjects(@"''\\''foo'\\''/bar'", @"'foo'/bar".singleQuotedShellWord); 54 | } 55 | 56 | - (void)testSingleQuotedShellWordWithSurroundingQuotes 57 | { 58 | XCTAssertEqualObjects(@"", [@"" singleQuotedShellWordWithSurroundingQuotes:NO]); 59 | XCTAssertEqualObjects(@"''", [@"" singleQuotedShellWordWithSurroundingQuotes:YES]); 60 | 61 | XCTAssertEqualObjects(@"'\\''", [@"'" singleQuotedShellWordWithSurroundingQuotes:NO]); 62 | XCTAssertEqualObjects(@"''\\'''", [@"'" singleQuotedShellWordWithSurroundingQuotes:YES]); 63 | 64 | XCTAssertEqualObjects(@"/foo/bar", [@"/foo/bar" singleQuotedShellWordWithSurroundingQuotes:NO]); 65 | XCTAssertEqualObjects(@"'/foo/bar'", [@"/foo/bar" singleQuotedShellWordWithSurroundingQuotes:YES]); 66 | 67 | XCTAssertEqualObjects(@"/foo bar/baz", [@"/foo bar/baz" singleQuotedShellWordWithSurroundingQuotes:NO]); 68 | XCTAssertEqualObjects(@"'/foo bar/baz'", [@"/foo bar/baz" singleQuotedShellWordWithSurroundingQuotes:YES]); 69 | 70 | XCTAssertEqualObjects(@"/Apple'\\''s Stuff", [@"/Apple's Stuff" singleQuotedShellWordWithSurroundingQuotes:NO]); 71 | XCTAssertEqualObjects(@"'/Apple'\\''s Stuff'", [@"/Apple's Stuff" singleQuotedShellWordWithSurroundingQuotes:YES]); 72 | 73 | XCTAssertEqualObjects(@"/foo/'\\''bar'\\''", [@"/foo/'bar'" singleQuotedShellWordWithSurroundingQuotes:NO]); 74 | XCTAssertEqualObjects(@"'/foo/'\\''bar'\\'''", [@"/foo/'bar'" singleQuotedShellWordWithSurroundingQuotes:YES]); 75 | 76 | XCTAssertEqualObjects(@"'\\''foo'\\''/bar", [@"'foo'/bar" singleQuotedShellWordWithSurroundingQuotes:NO]); 77 | XCTAssertEqualObjects(@"''\\''foo'\\''/bar'", [@"'foo'/bar" singleQuotedShellWordWithSurroundingQuotes:YES]); 78 | } 79 | 80 | - (void)testStringByRemovingSuffix 81 | { 82 | XCTAssertEqualObjects(@"", [@"" stringByRemovingSuffix:@","]); 83 | XCTAssertEqualObjects(@"", [@"," stringByRemovingSuffix:@","]); 84 | XCTAssertEqualObjects(@"one", [@"one," stringByRemovingSuffix:@","]); 85 | XCTAssertEqualObjects(@"one, two", [@"one, two" stringByRemovingSuffix:@","]); 86 | 87 | XCTAssertEqualObjects(@"", [@"" stringByRemovingSuffix:@";\r\n"]); 88 | XCTAssertEqualObjects(@"", [@";\r\n" stringByRemovingSuffix:@";\r\n"]); 89 | XCTAssertEqualObjects(@";\r", [@";\r" stringByRemovingSuffix:@";\r\n"]); 90 | XCTAssertEqualObjects(@"\r\n", [@"\r\n" stringByRemovingSuffix:@";\r\n"]); 91 | XCTAssertEqualObjects(@"one", [@"one;\r\n" stringByRemovingSuffix:@";\r\n"]); 92 | XCTAssertEqualObjects(@"one;\r\ntwo", [@"one;\r\ntwo" stringByRemovingSuffix:@";\r\n"]); 93 | } 94 | 95 | - (void)testWordsSeparatedByWhitespaceAndNewlineCharacters 96 | { 97 | NSArray *expected = @[]; 98 | XCTAssertEqualObjects(expected, @"".wordsSeparatedByWhitespaceAndNewlineCharacters); 99 | 100 | expected = @[ @"grep" ]; 101 | XCTAssertEqualObjects(expected, @"grep".wordsSeparatedByWhitespaceAndNewlineCharacters); 102 | 103 | expected = @[ @"grep" ]; 104 | XCTAssertEqualObjects(expected, @"\tgrep\n".wordsSeparatedByWhitespaceAndNewlineCharacters); 105 | 106 | expected = @[ @"grep(1)" ]; 107 | XCTAssertEqualObjects(expected, @"grep(1)".wordsSeparatedByWhitespaceAndNewlineCharacters); 108 | 109 | expected = @[ @"grep", @"find" ]; 110 | XCTAssertEqualObjects(expected, @"grep find".wordsSeparatedByWhitespaceAndNewlineCharacters); 111 | 112 | expected = @[ @"grep", @"find", @"open(2)" ]; 113 | XCTAssertEqualObjects(expected, @"grep find open(2)".wordsSeparatedByWhitespaceAndNewlineCharacters); 114 | 115 | expected = @[ @"grep(1)", @"find", @"open(2)", @"printf(3)" ]; 116 | XCTAssertEqualObjects(expected, @" grep(1)\r\nfind\t open(2)\n printf(3)\r\n".wordsSeparatedByWhitespaceAndNewlineCharacters); 117 | } 118 | 119 | @end 120 | -------------------------------------------------------------------------------- /ManOpenTests/NSURL+ManOpenTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+ManOpenTests.m 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 12/24/17. 6 | // 7 | 8 | #import 9 | #import "NSURL+ManOpen.h" 10 | 11 | 12 | @interface NSURL_ManOpenTests : XCTestCase 13 | @end 14 | 15 | 16 | @implementation NSURL_ManOpenTests 17 | 18 | #pragma mark - file: scheme 19 | 20 | - (void)testFileScheme 21 | { 22 | NSURL *url = [NSURL URLWithString:@"file:///usr/local/share/man/man1/wget.1"]; 23 | 24 | XCTAssertTrue(url.isFileScheme); 25 | XCTAssertFalse(url.isManOpenScheme); 26 | XCTAssertFalse(url.isXManPageScheme); 27 | 28 | url = [NSURL URLWithString:@"FILE:///usr/local/share/man/man1/wget.1"]; 29 | 30 | XCTAssertTrue(url.isFileScheme); 31 | XCTAssertFalse(url.isManOpenScheme); 32 | XCTAssertFalse(url.isXManPageScheme); 33 | 34 | url = [NSURL URLWithString:@"File:///usr/local/share/man/man1/wget.1"]; 35 | 36 | XCTAssertTrue(url.isFileScheme); 37 | XCTAssertFalse(url.isManOpenScheme); 38 | XCTAssertFalse(url.isXManPageScheme); 39 | } 40 | 41 | #pragma mark - manopen: scheme 42 | 43 | - (void)testManOpenScheme 44 | { 45 | NSURL *url = [NSURL URLWithString:@"manopen://1/printf"]; 46 | 47 | XCTAssertFalse(url.isFileScheme); 48 | XCTAssertTrue(url.isManOpenScheme); 49 | XCTAssertFalse(url.isXManPageScheme); 50 | 51 | url = [NSURL URLWithString:@"MANOPEN://1/printf"]; 52 | 53 | XCTAssertFalse(url.isFileScheme); 54 | XCTAssertTrue(url.isManOpenScheme); 55 | XCTAssertFalse(url.isXManPageScheme); 56 | 57 | url = [NSURL URLWithString:@"ManOpen://1/printf"]; 58 | 59 | XCTAssertFalse(url.isFileScheme); 60 | XCTAssertTrue(url.isManOpenScheme); 61 | XCTAssertFalse(url.isXManPageScheme); 62 | } 63 | 64 | #pragma mark - x-man-page: scheme 65 | 66 | - (void)testXManPageScheme 67 | { 68 | NSURL *url = [NSURL URLWithString:@"x-man-page://1/printf"]; 69 | 70 | XCTAssertFalse(url.isFileScheme); 71 | XCTAssertFalse(url.isManOpenScheme); 72 | XCTAssertTrue(url.isXManPageScheme); 73 | 74 | url = [NSURL URLWithString:@"X-MAN-PAGE://1/printf"]; 75 | 76 | XCTAssertFalse(url.isFileScheme); 77 | XCTAssertFalse(url.isManOpenScheme); 78 | XCTAssertTrue(url.isXManPageScheme); 79 | 80 | url = [NSURL URLWithString:@"X-Man-Page://1/printf"]; 81 | 82 | XCTAssertFalse(url.isFileScheme); 83 | XCTAssertFalse(url.isManOpenScheme); 84 | XCTAssertTrue(url.isXManPageScheme); 85 | } 86 | 87 | @end 88 | -------------------------------------------------------------------------------- /ManOpenTests/NSUserDefaults+ManOpenPreferencesTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSUserDefaults+ManOpenPreferencesTests.m 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 2/10/18. 6 | // 7 | 8 | #import 9 | #import "NSUserDefaults+ManOpen.h" 10 | 11 | 12 | @interface NSUserDefaults_ManOpenPreferencesTests : XCTestCase 13 | 14 | @property (retain) NSUserDefaults *defaults; 15 | 16 | @end 17 | 18 | 19 | @implementation NSUserDefaults_ManOpenPreferencesTests 20 | 21 | #pragma mark - colors 22 | 23 | - (void)testManTextColor 24 | { 25 | NSData *redData = [NSArchiver archivedDataWithRootObject:[NSColor systemRedColor]]; 26 | [_defaults setObject:redData forKey:@"ManTextColor"]; 27 | NSColor *textColor = [_defaults manTextColor]; 28 | XCTAssertEqualObjects([NSColor systemRedColor], textColor); 29 | } 30 | 31 | - (void)testManTextColor_when_nil 32 | { 33 | [_defaults removeObjectForKey:@"ManTextColor"]; 34 | NSColor *textColor = [_defaults manTextColor]; 35 | XCTAssertEqualObjects([NSColor textColor], textColor); 36 | } 37 | 38 | - (void)testManLinkColor 39 | { 40 | NSData *redData = [NSArchiver archivedDataWithRootObject:[NSColor systemRedColor]]; 41 | [_defaults setObject:redData forKey:@"ManLinkColor"]; 42 | NSColor *linkColor = [_defaults manLinkColor]; 43 | XCTAssertEqualObjects([NSColor systemRedColor], linkColor); 44 | } 45 | 46 | - (void)testManLinkColor_when_nil 47 | { 48 | [_defaults removeObjectForKey:@"ManLinkColor"]; 49 | NSColor *linkColor = [_defaults manLinkColor]; 50 | NSColor *expectedColor = [NSColor colorWithDeviceRed:0.1f green:0.1f blue:1.0f alpha:1.0f]; 51 | XCTAssertEqualObjects(expectedColor, linkColor); 52 | } 53 | 54 | - (void)testManBackgroundColor 55 | { 56 | NSData *redData = [NSArchiver archivedDataWithRootObject:[NSColor systemRedColor]]; 57 | [_defaults setObject:redData forKey:@"ManBackgroundColor"]; 58 | NSColor *backgroundColor = [_defaults manBackgroundColor]; 59 | XCTAssertEqualObjects([NSColor systemRedColor], backgroundColor); 60 | } 61 | 62 | - (void)testManBackgroundColor_when_nil 63 | { 64 | [_defaults removeObjectForKey:@"ManBackgroundColor"]; 65 | NSColor *backgroundColor = [_defaults manBackgroundColor]; 66 | XCTAssertEqualObjects([NSColor textBackgroundColor], backgroundColor); 67 | } 68 | 69 | #pragma mark - manFont 70 | 71 | - (void)testManFont 72 | { 73 | [_defaults setObject:@"14.0 HelveticaNeue" forKey:@"ManFont"]; 74 | NSFont *font = [_defaults manFont]; 75 | XCTAssertEqualObjects(@"HelveticaNeue", font.fontName); 76 | XCTAssertEqual(14.0, font.pointSize); 77 | } 78 | 79 | - (void)testManFont_when_nil 80 | { 81 | [_defaults removeObjectForKey:@"ManFont"]; 82 | NSFont *font = [_defaults manFont]; 83 | XCTAssertTrue(font.isFixedPitch); 84 | XCTAssertEqual(12.0, font.pointSize); 85 | } 86 | 87 | - (void)testManFont_when_missing_space 88 | { 89 | [_defaults setObject:@"HelveticaNeue" forKey:@"ManFont"]; 90 | NSFont *font = [_defaults manFont]; 91 | XCTAssertTrue(font.isFixedPitch); 92 | XCTAssertEqual(12.0, font.pointSize); 93 | } 94 | 95 | - (void)testManFont_when_invalid_size 96 | { 97 | [_defaults setObject:@"NotASize HelveticaNeue" forKey:@"ManFont"]; 98 | NSFont *font = [_defaults manFont]; 99 | XCTAssertEqualObjects(@"HelveticaNeue", font.fontName); 100 | XCTAssertEqual(12.0, font.pointSize); 101 | } 102 | 103 | - (void)testManFont_when_invalid_name 104 | { 105 | [_defaults setObject:@"14.0 NotAFontName" forKey:@"ManFont"]; 106 | NSFont *font = [_defaults manFont]; 107 | XCTAssertTrue(font.isFixedPitch); 108 | XCTAssertEqual(12.0, font.pointSize); 109 | } 110 | 111 | #pragma mark - manPath 112 | 113 | - (void)testManPath 114 | { 115 | [_defaults setObject:@"/usr/man" forKey:@"ManPath"]; 116 | XCTAssertEqualObjects(@"/usr/man", [_defaults manPath]); 117 | } 118 | 119 | #pragma mark - setUp 120 | 121 | - (void)setUp 122 | { 123 | [super setUp]; 124 | _defaults = [NSUserDefaults new]; 125 | } 126 | 127 | @end 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ManOpen 2 | 3 | _ManOpen_ is a macOS GUI application for viewing Unix manual ("man") pages. 4 | This repository is a fork of [ManOpen 2.6][11] by [Carl Lindberg][12]. 5 | 6 | [![GitHub Actions][13]][14] [![Code Coverage][15]][16] 7 | 8 | [11]: http://clindberg.org/projects/ManOpen.html 9 | [12]: mailto:lindberg@clindberg.org 10 | [13]: https://github.com/donmccaughey/ManOpen/actions/workflows/tests.yml/badge.svg 11 | [14]: https://github.com/donmccaughey/ManOpen/actions/workflows/tests.yml 12 | [15]: https://codecov.io/gh/donmccaughey/ManOpen/branch/master/graph/badge.svg 13 | [16]: https://codecov.io/gh/donmccaughey/ManOpen 14 | 15 | ## Features 16 | 17 | - Graphical interface for viewing Unix _man_ pages 18 | - Open _man_ page by name or manual section + name 19 | - Search all _man_ pages by keyword, using the `apropos` command 20 | - Hyperlinks to related _man_ pages 21 | - Quick section navigation 22 | - Open _man_ files by path or `file:` URL 23 | - Supports `x-man-page:` URLs to open a _man_ page or perform an `apropos` search 24 | - `openman` command line tool to start _ManOpen_ from a terminal 25 | 26 | ## Requirements 27 | 28 | _ManOpen_ is built on macOS High Sierra 10.13 using Xcode 9.3. It should run on 29 | OS X Yosemite 10.10 and later. 30 | 31 | ## License 32 | 33 | For the files [`cat2html.l`][41] and [`cat2rtf.l`][42], "Permission is granted to 34 | use this code." All other files in the _ManOpen_ project are available under a 35 | BSD-style license; see the [`LICENSE`][43] file for details. 36 | 37 | [41]: ./cat2html/cat2html.l 38 | [42]: ./cat2rtf/cat2rtf.l 39 | [43]: ./LICENSE 40 | 41 | ## History 42 | 43 | _ManOpen 1.0_ was originally released for NextSTEP by [Harald Schlangmann][51] 44 | in 1993. The first Mac OS X release, _ManOpen 2.0_, was released in October 45 | 1999 by Carl Lindberg. _ManOpen 2.6_, from which this version is forked, was 46 | released in March 2012. 47 | 48 | [51]: mailto:schlangm@informatik.uni-muenchen.de 49 | 50 | ## Related 51 | 52 | [_ManDrake_][61] is an [open source][62] macOS app by [Sveinbjörn Þórðarson][63] 53 | for creating and editing man pages. 54 | 55 | [61]: http://sveinbjorn.org/mandrake 56 | [62]: https://github.com/sveinbjornt/ManDrake 57 | [63]: http://sveinbjorn.org 58 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # ManOpen To Do 2 | 3 | ## Fix Bugs 4 | 5 | - The _Edit_ | _Copy URL_ command produces clipboard text like ``, 6 | but this should be `x-man-page:///grep` 7 | - The _Edit_ | _Find_ | _Find..._ command shows a Find dialog with Replace field and actions, 8 | show a Find only dialog instead 9 | - In the `ManDocument` class, the `copyURL` instance variable will be mistakenly set to 10 | something like `x-man-doc://2/open(2)` 11 | - When `MANPATH` is nil or not provided, apropos and man page searches should use the 12 | `MANPATH` stored in user defaults; currently only some code paths do this. 13 | - Validate that `manopen:` scheme handles all `MANSECT`s given in `man.conf` and section 14 | names in `openman.m`. 15 | - In `ManDocumentController`'s `-openString:` method, employ a definitive method for 16 | breaking the string into man pages to open, removing _approximately_ from the 17 | `informativeText` of the alert. 18 | - When opening or re-opening man pages that are aliases for the same file (e.g. `grep` and 19 | `egrep`), locate the already-opened man page for that alias. 20 | - When opening man pages from the File | Open Recent menu, the ManDocument window 21 | shows the filename as the title (e.g. `mkfifo.1`) and a default document icon; figure out 22 | how to reopen the document with the same title and icon used to originally open it. 23 | 24 | ## Modernize Code 25 | 26 | - Convert `ManOpen.scriptSuite` and `ManOpen.scriptTerminology` to `sdef` format, 27 | see https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ScriptableCocoaApplications/SApps_creating_sdef/SAppsCreateSdef.html 28 | and https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ScriptableCocoaApplications/SApps_suites/SAppsSuites.html 29 | - Update `Services` array in `Info-ManOpen.plist` 30 | 1. Remove `ManOpen/` prefix from menu item titles 31 | 1. Add `NSServiceDescription` keys 32 | 1. Add `NSRequiredContext` keys 33 | 1. In the "Open File" service method `-openFiles:userData:error:`, the error out-param 34 | is no longer working since the asynchronous 35 | `-openDocumentWithContentsOfURL:display:completionHandler:` call has replaced 36 | the synchronous `-openDocumentWithContentsOfURL:display:error:` call. 37 | - Convert `openman` to ARC 38 | - Convert `ManOpen` app to ARC 39 | - Convert `ManOpenTests` to ARC 40 | - Replace `NSObject (PoofDragDataSource)` category with a better mechanism for 41 | defining the selector `-tableView:performDropOutsideViewAtPoint:` 42 | - Locate and replace uses of macros `IsLeopard()`, `IsSnowLeopard()` and `IsLion()` 43 | defined in `SystemType.h` 44 | - Audit and remove definitions in `SystemType.h` 45 | 46 | ## New Features 47 | 48 | - Automatically locate Xcode and search it for man pages 49 | - Distribute `openman` in app bundle and add command to install it 50 | - Make sure that `ManOpen.app` can always find the `openman.1` man page. 51 | - Make sure that `ManOpen.app` registers with Launch Services on startup by calling 52 | `LSRegisterURL()` 53 | - Redraw open windows after changing font in preferences. 54 | - Recalculate window sizes after changing font in preferences. 55 | -------------------------------------------------------------------------------- /cat2html/cat2html.l: -------------------------------------------------------------------------------- 1 | /* 2 | cat2html.l: cat to HTML converter specification 3 | (c) 1993 by Harald Schlangmann 4 | Permission is granted to use this code. Send additions 5 | and bug reports to my address below. 6 | 7 | v1.0 Harald Schlangmann, July 20 1993 8 | schlangm@informatik.uni-muenchen.de 9 | v1.1 Bold style x^H{x^H}* implemented. 10 | 11 | v2.0 Carl Lindberg lindberg@mac.com 12 | Added blank line suppressing. 13 | v2.1 New cat2html to spit out HTML with links -CEL 14 | 15 | */ 16 | 17 | #include 18 | 19 | #define BOLDFLAG 1 20 | #define ULINEFLAG 2 21 | 22 | int flags = 0, neededflags = 0; 23 | 24 | #define SETB neededflags |= BOLDFLAG 25 | #define UNSETB neededflags &= ~BOLDFLAG 26 | #define SETU neededflags |= ULINEFLAG 27 | #define UNSETU neededflags &= ~ULINEFLAG 28 | 29 | /* 30 | * Default settings, may be changed using options... 31 | */ 32 | 33 | static char *startBold = ""; 34 | static char *stopBold = ""; 35 | static char *startULine = ""; 36 | static char *stopULine = ""; 37 | static char *startHeader = ""; 38 | static char *stopHeader = ""; 39 | static int addLinks = 0; 40 | static int markHeaders = 0; 41 | static int lineCount = 0; 42 | static int maxLineCount = 3; 43 | 44 | 45 | /* Decode a UTF8 sequence to return the unicode number */ 46 | static unsigned decodeUTF8(unsigned char *buf, yy_size_t len) 47 | { 48 | int n = buf[0] & (0x7f >> len); 49 | yy_size_t i; 50 | 51 | for (i=1; i
",stdout);
 65 |     }
 66 |     
 67 |     static void emitPostamble(void)
 68 |     {
 69 |         fputs("
\n",stdout); 70 | } 71 | 72 | #define adjust() if( neededflags!=flags ) _adjust() 73 | 74 | static void _adjust(void) { 75 | 76 | if( (flags^neededflags)&ULINEFLAG ) 77 | fputs(neededflags&ULINEFLAG? 78 | startULine:stopULine,stdout); 79 | if( (flags^neededflags)&BOLDFLAG ) 80 | fputs(neededflags&BOLDFLAG? 81 | startBold:stopBold,stdout); 82 | flags = neededflags; 83 | } 84 | 85 | static void emitChar(int ch) { 86 | adjust(); 87 | 88 | if (ch=='\n') { 89 | if (lineCount > maxLineCount) return; 90 | lineCount++; 91 | } 92 | else lineCount = 0; 93 | 94 | switch(ch) { 95 | case '"': fputs(""",stdout); break; 96 | case '<': fputs("<" ,stdout); break; 97 | case '>': fputs(">" ,stdout); break; 98 | case '&': fputs("&" ,stdout); break; 99 | default: fputc(ch,stdout); 100 | } 101 | } 102 | 103 | static void emitString(char *string) 104 | { 105 | int i, len=(int)strlen(string); 106 | for (i=0;i= 4 && charblock[3] == '\010') 121 | return; 122 | 123 | /* If the characters are equal, they are printed on top of each other, so make it bold */ 124 | if( charblock[0] == charblock[2] ) { 125 | if (doBold) SETB; 126 | emitChar(charblock[0]); 127 | if (doBold) UNSETB; 128 | } 129 | /* Otherwise, just emit the second character. */ 130 | else { 131 | #ifdef DEBUGBACKSPACE 132 | fprintf(stderr, "Unknown backspace pair %c and %c\n", charblock[0], charblock[2]); 133 | #endif 134 | emitChar(charblock[2]); 135 | } 136 | } 137 | 138 | static void emitBackspacedText(char *text, yy_size_t length) 139 | { 140 | yy_size_t i=0; 141 | while (i < length) { 142 | if ((i < (length-1)) && text[i+1] == '\010') { 143 | emitBackspacedLetters(&text[i], 3, 0); 144 | i+=3; 145 | } 146 | else { 147 | emitChar(text[i]); 148 | i++; 149 | } 150 | } 151 | } 152 | 153 | /* Use hexidecimal entities */ 154 | static void emitUnicode(unsigned charNum) 155 | { 156 | char entityBuf[20]; 157 | sprintf(entityBuf, "&#x%x;", charNum); 158 | emitRaw(entityBuf); 159 | } 160 | 161 | 162 | ALLBUTUL [^\n_\010] 163 | NEEDQUOTE [<>&"] 164 | VERBATIM [^_\n\010\x1B<>&( \t"\xC2-\xF4] 165 | UPPER [A-Z] 166 | UPPERCONT [A-Z0-9 \t()] 167 | UPPERBS {UPPER}\010{UPPER} 168 | UPPERBSCONT ({UPPERBS}|[ \t()]) 169 | UTF8START [\xC2-\xF4] 170 | UTF8CONT [\x80-\xBF] 171 | UTF8SEQ {UTF8START}({UTF8CONT}{1,3}) 172 | SGRSTART \x1B\[ 173 | 174 | %option debug 175 | %option noyywrap 176 | %option noinput 177 | %option 8bit 178 | %option prefix="cat2html" 179 | 180 | %x FIRSTLINE 181 | 182 | %% 183 | 184 | /* 185 | * Start state FIRSTLINE is used to treat the first non-empty 186 | * line special. (First line contains header). 187 | */ 188 | 189 | /* Some X.org X11 pages have a weird #pragma at the start; strip it out. */ 190 | "#pragma".*\n {} 191 | 192 | . { SETB; emitChar(yytext[0]); } 193 | 194 | .\n { 195 | SETB; 196 | emitChar(yytext[0]); 197 | UNSETB; 198 | emitChar('\n'); 199 | BEGIN(INITIAL); 200 | } 201 | 202 | \n { UNSETB; emitChar('\n'); } 203 | 204 | /* Part of the X11 thing gets put on a separate line by nroff, sigh. */ 205 | ^"vate/var/tmp/X11".*\n {} 206 | 207 | /* 208 | * Put a special HREF around likely looking links to other man pages 209 | */ 210 | [_a-zA-Z][-a-zA-Z0-9._]*(-[ \t\n]+[-a-zA-Z0-9._]+)?[ \t\n]*"("[1-9n][a-zA-Z]?")" { 211 | 212 | if (!addLinks) 213 | { 214 | emitString(yytext); 215 | } 216 | else 217 | { 218 | int i; 219 | char href[yyleng+1]; 220 | 221 | /* Change newlines to spaces in the href portion */ 222 | strcpy(href, yytext); 223 | for(i=0; i"); 229 | emitString(yytext); 230 | emitRaw(""); 231 | } 232 | } 233 | 234 | 235 | /* 236 | * Non-empty, all-uppercase lines are treated as headers 237 | */ 238 | ^{UPPER}{UPPERCONT}*$ { 239 | SETB; 240 | if (markHeaders) emitRaw(startHeader); 241 | emitString(yytext); 242 | if (markHeaders) emitRaw(stopHeader); 243 | UNSETB; 244 | emitChar('\n'); 245 | } 246 | 247 | /* Similar for all-uppercase lines that use backspace for bolding */ 248 | ^{UPPERBS}{UPPERBSCONT}*$ { 249 | SETB; 250 | if (markHeaders) emitRaw(startHeader); 251 | emitBackspacedText(yytext, yyleng); 252 | if (markHeaders) emitRaw(stopHeader); 253 | UNSETB; 254 | emitChar('\n'); 255 | } 256 | 257 | /* 258 | * nroff +- 259 | */ 260 | 261 | "+"\010_ emitRaw("±"); 262 | 263 | /* 264 | * underline (part 1) 265 | */ 266 | 267 | {ALLBUTUL}\010_ { 268 | SETU; 269 | emitChar(yytext[0]); 270 | UNSETU; 271 | } 272 | 273 | /* 274 | * nroff bullets 275 | */ 276 | o\010"+" emitRaw("·"); /* "•" doesn't work */ 277 | "+"\010o emitRaw("·"); 278 | o\010o\010"+"\010"+" emitRaw("·"); 279 | "+"\010"+\010"o\010o emitRaw("·"); 280 | 281 | /* 282 | * underline (part 2) 283 | */ 284 | 285 | _\010{ALLBUTUL} { 286 | SETU; 287 | emitChar(yytext[2]); 288 | UNSETU; 289 | } 290 | 291 | /* 292 | * handle further BS combinations 293 | */ 294 | 295 | .\010.\010? { 296 | emitBackspacedLetters(yytext, yyleng, 1); 297 | } 298 | 299 | /* Same idea but with UTF-8 characters */ 300 | {UTF8SEQ}\010{UTF8SEQ}\010? { 301 | if (yytext[yyleng-1] != '\010') { 302 | char *backspace = index(yytext, '\010'); 303 | if (backspace != NULL) { 304 | emitUnicode(decodeUTF8((unsigned char *)backspace+1, (yyleng - (backspace-yytext) - 1))); 305 | } 306 | } 307 | } 308 | 309 | /* If we find a UTF8 sequence, decode it */ 310 | {UTF8SEQ} { 311 | emitUnicode(decodeUTF8((unsigned char *)yytext, yyleng)); 312 | } 313 | 314 | /* Some versions of nroff/grotty use SGR escape sequences instead of the backspace hacks */ 315 | {SGRSTART}0?m UNSETU;UNSETB; 316 | {SGRSTART}1m SETB; 317 | {SGRSTART}[347]m SETU; 318 | {SGRSTART}(21|22)m UNSETB; 319 | {SGRSTART}(23|24|27)m UNSETU; 320 | {SGRSTART}[0-9;]+m {/*ignore any other codes*/} 321 | 322 | /* 323 | group characters in VERBATIM to make this 324 | filter faster... 325 | */ 326 | [ \t\n]+ emitString(yytext); 327 | {VERBATIM}+/[^\010] emitString(yytext); 328 | 329 | /* 330 | remaining specials 331 | */ 332 | 333 | /* \n emitChar('\n'); */ /*Matched by above whitespace matching rule */ 334 | . emitChar(yytext[0]); 335 | 336 | %% 337 | 338 | static void usage() { 339 | 340 | fprintf(stderr,"Usage: cat2html [-il] []\n" 341 | "\tTranslate output of (g)nroff to HTML. If no\n" 342 | "\t is given, cat2html reads stdin.\n" 343 | "\toption -i uses italic characters for underlining.\n" 344 | "\toption -l adds 'manpage:' HREF links to other man pages.\n" 345 | "\tHTML output is sent to stdout.\n"); 346 | exit(1); 347 | } 348 | 349 | int main(int argc, char *argv[]) 350 | { 351 | int c; 352 | 353 | yy_flex_debug = 0; 354 | 355 | /* Keep the same args as cat2rtf, even though -s doesn't really make much difference */ 356 | while ((c = getopt(argc, argv, "dgGiISs:lH")) != EOF) 357 | { 358 | switch( c ) { 359 | case 'd': 360 | yy_flex_debug = 1; 361 | break; 362 | case 'g': 363 | case 'G': 364 | startBold = ""; // ""; 365 | stopBold = ""; // ""; 366 | break; 367 | case 'i': 368 | case 'I': 369 | startULine = ""; 370 | stopULine = ""; 371 | break; 372 | case 's': 373 | maxLineCount = atoi(optarg); 374 | break; 375 | case 'S': 376 | maxLineCount = -1; 377 | break; 378 | case 'l': 379 | addLinks = 1; 380 | break; 381 | case 'H': 382 | markHeaders = 1; 383 | break; 384 | case '?': 385 | default: 386 | usage(); 387 | } 388 | } 389 | 390 | if( optind < argc ) 391 | yyin = fopen(argv[optind], "r"); 392 | else 393 | yyin = stdin; 394 | 395 | emitPreamble(); 396 | BEGIN(FIRSTLINE); 397 | yylex(); 398 | emitPostamble(); 399 | 400 | /* Shuts up a compiler warning */ 401 | if (0) unput('h'); 402 | 403 | return 0; 404 | } 405 | -------------------------------------------------------------------------------- /openman/Application.h: -------------------------------------------------------------------------------- 1 | // 2 | // Application.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/13/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | @class Version; 12 | 13 | @protocol LaunchServices; 14 | 15 | 16 | @interface Application : NSObject 17 | 18 | @property (copy) NSString *bundleIdentifier; 19 | @property (retain) NSURL *url; 20 | @property (retain) Version *version; 21 | 22 | + (instancetype)latestVersionWithLaunchServices:(id)launchServices 23 | bundleIdentifier:(NSString *)bundleIdentifier 24 | error:(NSError **)error; 25 | 26 | - (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier 27 | URL:(NSURL *)url 28 | andVersion:(Version *)version; 29 | 30 | - (instancetype)initWithBundle:(NSBundle *)bundle; 31 | 32 | - (instancetype)initWithURL:(NSURL *)url; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /openman/Application.m: -------------------------------------------------------------------------------- 1 | // 2 | // Application.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/13/18. 6 | // 7 | 8 | #import "Application.h" 9 | 10 | #import "LaunchServices.h" 11 | #import "Version.h" 12 | 13 | 14 | @implementation Application 15 | 16 | + (instancetype)latestVersionWithLaunchServices:(id)launchServices 17 | bundleIdentifier:(NSString *)bundleIdentifier 18 | error:(NSError **)error 19 | { 20 | NSArray *applications = [launchServices applicationsForBundleIdentifier:bundleIdentifier 21 | error:error]; 22 | if (applications.count) { 23 | NSArray *sortByVersion = @[ 24 | [NSSortDescriptor sortDescriptorWithKey:@"version" ascending:NO], 25 | ]; 26 | NSArray *sorted = [applications sortedArrayUsingDescriptors:sortByVersion]; 27 | return sorted.firstObject; 28 | } else { 29 | return nil; 30 | } 31 | } 32 | 33 | - (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier 34 | URL:(NSURL *)url 35 | andVersion:(Version *)version 36 | { 37 | self = [super init]; 38 | if (self) { 39 | _bundleIdentifier = [bundleIdentifier copy]; 40 | _url = [url retain]; 41 | _version = [version retain]; 42 | } 43 | return self; 44 | } 45 | 46 | - (instancetype)initWithBundle:(NSBundle *)bundle 47 | { 48 | if (!bundle) return nil; 49 | 50 | NSString *versionString = [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]; 51 | Version *version = [[[Version alloc] initWithVersion:versionString] autorelease]; 52 | return [self initWithBundleIdentifier:bundle.bundleIdentifier 53 | URL:bundle.bundleURL 54 | andVersion:version]; 55 | } 56 | 57 | - (instancetype)initWithURL:(NSURL *)url 58 | { 59 | NSBundle *bundle = [NSBundle bundleWithURL:url]; 60 | return [self initWithBundle:bundle]; 61 | } 62 | 63 | - (void)dealloc 64 | { 65 | [_bundleIdentifier release]; 66 | [_url release]; 67 | [_version release]; 68 | [super dealloc]; 69 | } 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /openman/ApplicationTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationTests.m 3 | // ManOpenTests 4 | // 5 | // Created by Don McCaughey on 2/13/18. 6 | // 7 | 8 | #import 9 | #import "Application.h" 10 | #import "LaunchServicesFake.h" 11 | #import "Version.h" 12 | 13 | 14 | @interface ApplicationTests : XCTestCase 15 | 16 | @property (retain) LaunchServicesFake *launchServicesFake; 17 | @property (retain) Application *application1; 18 | @property (retain) Application *application2; 19 | @property (retain) Application *application3; 20 | 21 | @end 22 | 23 | 24 | @implementation ApplicationTests 25 | 26 | #pragma mark - latestVersionWithBundleIdentifier: 27 | 28 | - (void)testLatestVersionWithLaunchServicesBundleIdentifier_when_not_found 29 | { 30 | _launchServicesFake.errorOut = nil; 31 | _launchServicesFake.return_applications = @[]; 32 | 33 | NSError *error = nil; 34 | Application *application = [Application latestVersionWithLaunchServices:_launchServicesFake 35 | bundleIdentifier:@"cc.donm.ManOpen" 36 | error:&error]; 37 | 38 | XCTAssertNil(application); 39 | XCTAssertNil(error); 40 | } 41 | 42 | - (void)testLatestVersionWithLaunchServicesBundleIdentifier_when_error_returned 43 | { 44 | _launchServicesFake.errorOut = [NSError errorWithDomain:NSCocoaErrorDomain 45 | code:-1 46 | userInfo:nil]; 47 | _launchServicesFake.return_applications = nil; 48 | 49 | NSError *error = nil; 50 | Application *application = [Application latestVersionWithLaunchServices:_launchServicesFake 51 | bundleIdentifier:@"cc.donm.ManOpen" 52 | error:&error]; 53 | 54 | XCTAssertNil(application); 55 | XCTAssertNotNil(error); 56 | } 57 | 58 | - (void)testLatestVersionWithLaunchServicesBundleIdentifier_when_one_found 59 | { 60 | _launchServicesFake.errorOut = nil; 61 | _launchServicesFake.return_applications = @[ _application1 ]; 62 | 63 | NSError *error = nil; 64 | Application *application = [Application latestVersionWithLaunchServices:_launchServicesFake 65 | bundleIdentifier:@"cc.donm.ManOpen" 66 | error:&error]; 67 | 68 | XCTAssertNotNil(application); 69 | XCTAssertEqualObjects(@"2.6", application.version.description); 70 | XCTAssertNil(error); 71 | } 72 | 73 | - (void)testLatestVersionWithLaunchServicesBundleIdentifier_when_three_found 74 | { 75 | _launchServicesFake.errorOut = nil; 76 | _launchServicesFake.return_applications = @[ _application1, _application2, _application3 ]; 77 | 78 | NSError *error = nil; 79 | Application *application = [Application latestVersionWithLaunchServices:_launchServicesFake 80 | bundleIdentifier:@"cc.donm.ManOpen" 81 | error:&error]; 82 | 83 | XCTAssertNotNil(application); 84 | XCTAssertEqualObjects(@"2.8", application.version.description); 85 | XCTAssertNil(error); 86 | } 87 | 88 | #pragma mark - initWithBundleIdentifier:URL:andVersion: 89 | 90 | - (void)testInitWithBundleIdentifierURLandVersion 91 | { 92 | NSURL *url = [NSURL URLWithString:@"file:///Applications/MyApp.app"]; 93 | Version *version = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 94 | Application *application = [[Application alloc] initWithBundleIdentifier:@"com.example.MyApp" 95 | URL:url 96 | andVersion:version]; 97 | [application autorelease]; 98 | XCTAssertEqualObjects(@"com.example.MyApp", application.bundleIdentifier); 99 | XCTAssertEqualObjects(url, application.url); 100 | XCTAssertEqualObjects(version, application.version); 101 | } 102 | 103 | #pragma mark - initWithBundle: 104 | 105 | - (void)testInitWithBundle 106 | { 107 | NSBundle *bundle = [NSBundle bundleWithIdentifier:@"com.apple.dt.Xcode"]; 108 | XCTAssertNotNil(bundle); 109 | 110 | Application *application = [[[Application alloc] initWithBundle:bundle] autorelease]; 111 | XCTAssertNotNil(application); 112 | XCTAssertEqualObjects(@"com.apple.dt.Xcode", application.bundleIdentifier); 113 | XCTAssertNotNil(application.url); 114 | XCTAssertNotNil(application.version); 115 | XCTAssertTrue(application.version.major > 0); 116 | } 117 | 118 | - (void)testInitWithBundle_when_nil 119 | { 120 | Application *application = [[Application alloc] initWithBundle:nil]; 121 | XCTAssertNil(application); 122 | } 123 | 124 | #pragma mark - initWithURL: 125 | 126 | - (void)testInitWithURL 127 | { 128 | NSURL *url = [NSURL URLWithString:@"file:///Applications/Xcode.app"]; 129 | Application *application = [[[Application alloc] initWithURL:url] autorelease]; 130 | XCTAssertEqualObjects(@"com.apple.dt.Xcode", application.bundleIdentifier); 131 | XCTAssertNotNil(application.url); 132 | XCTAssertNotNil(application.version); 133 | XCTAssertTrue(application.version.major > 0); 134 | } 135 | 136 | - (void)testInitWithURL_when_invalid_path 137 | { 138 | NSURL *url = [NSURL URLWithString:@"file:///not/a/real.app"]; 139 | Application *application = [[Application alloc] initWithURL:url]; 140 | XCTAssertNil(application); 141 | } 142 | 143 | - (void)testInitWithURL_when_invalid_scheme 144 | { 145 | NSURL *url = [NSURL URLWithString:@"http://www.example.com"]; 146 | XCTAssertThrows( 147 | [[Application alloc] initWithURL:url] 148 | ); 149 | } 150 | 151 | #pragma mark - setUp 152 | 153 | - (void)setUp 154 | { 155 | [super setUp]; 156 | _launchServicesFake = [LaunchServicesFake new]; 157 | 158 | NSURL *url1 = [NSURL URLWithString:@"file:///Applications/ManOpen.app/"]; 159 | Version *version1 = [[[Version alloc] initWithVersion:@"2.6"] autorelease]; 160 | _application1 = [[Application alloc] initWithBundleIdentifier:@"cc.donm.ManOpen" 161 | URL:url1 162 | andVersion:version1]; 163 | 164 | NSURL *url2 = [NSURL URLWithString:@"file:///Applications/ManOpen-2.7.app/"]; 165 | Version *version2 = [[[Version alloc] initWithVersion:@"2.7"] autorelease]; 166 | _application2 = [[Application alloc] initWithBundleIdentifier:@"cc.donm.ManOpen" 167 | URL:url2 168 | andVersion:version2]; 169 | 170 | NSURL *url3 = [NSURL URLWithString:@"file:///Applications/ManOpen-2.8.app/"]; 171 | Version *version3 = [[[Version alloc] initWithVersion:@"2.8"] autorelease]; 172 | _application3 = [[Application alloc] initWithBundleIdentifier:@"cc.donm.ManOpen" 173 | URL:url3 174 | andVersion:version3]; 175 | } 176 | 177 | - (void)tearDown 178 | { 179 | [super tearDown]; 180 | [_launchServicesFake release]; 181 | [_application1 release]; 182 | [_application2 release]; 183 | [_application3 release]; 184 | } 185 | 186 | @end 187 | -------------------------------------------------------------------------------- /openman/LaunchServices.h: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchServices.h 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/14/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | @class Application; 12 | 13 | 14 | @protocol LaunchServices 15 | 16 | - (NSArray *)applicationsForBundleIdentifier:(NSString *)bundleIdentifier 17 | error:(NSError **)error; 18 | 19 | - (BOOL)openItemURLs:(NSArray *)itemURLs 20 | inApplication:(Application *)application 21 | error:(NSError **)error; 22 | 23 | @end 24 | 25 | 26 | @interface LaunchServices : NSObject 27 | 28 | - (NSArray *)bundlesForBundleIdentifier:(NSString *)bundleIdentifier 29 | error:(NSError **)error; 30 | 31 | - (BOOL)openItemURLs:(NSArray *)itemURLs 32 | inAppURL:(NSURL *)appURL 33 | error:(NSError **)error; 34 | 35 | - (NSArray *)URLsForBundleIdentifier:(NSString *)bundleIdentifier 36 | error:(NSError **)error; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /openman/LaunchServices.m: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchServices.m 3 | // ManOpen 4 | // 5 | // Created by Don McCaughey on 2/14/18. 6 | // 7 | 8 | #import "LaunchServices.h" 9 | 10 | #import 11 | 12 | #import "Application.h" 13 | 14 | 15 | @implementation LaunchServices 16 | 17 | - (NSArray *)applicationsForBundleIdentifier:(NSString *)bundleIdentifier 18 | error:(NSError **)error 19 | { 20 | NSArray *bundles = [self bundlesForBundleIdentifier:bundleIdentifier 21 | error:error]; 22 | if (bundles) { 23 | NSMutableArray *applications = [[NSMutableArray new] autorelease]; 24 | for (NSBundle *bundle in bundles) { 25 | Application *application = [[[Application alloc] initWithBundle:bundle] autorelease]; 26 | [applications addObject:application]; 27 | } 28 | return applications; 29 | } else { 30 | return nil; 31 | } 32 | } 33 | 34 | - (NSArray *)bundlesForBundleIdentifier:(NSString *)bundleIdentifier 35 | error:(NSError **)error 36 | { 37 | NSArray *urls = [self URLsForBundleIdentifier:bundleIdentifier 38 | error:error]; 39 | if (urls) { 40 | NSMutableArray *bundles = [[NSMutableArray new] autorelease]; 41 | for (NSURL *url in urls) { 42 | NSBundle *bundle = [NSBundle bundleWithURL:url]; 43 | [bundles addObject:bundle]; 44 | } 45 | return bundles; 46 | } else { 47 | return nil; 48 | } 49 | } 50 | 51 | - (BOOL)openItemURLs:(NSArray *)itemURLs 52 | inApplication:(Application *)application 53 | error:(NSError **)error 54 | { 55 | return [self openItemURLs:itemURLs 56 | inAppURL:application.url 57 | error:error]; 58 | } 59 | 60 | - (BOOL)openItemURLs:(NSArray *)itemURLs 61 | inAppURL:(NSURL *)appURL 62 | error:(NSError **)error 63 | { 64 | LSLaunchFlags launchFlags = kLSLaunchAsync | kLSLaunchDontSwitch; 65 | LSLaunchURLSpec launchURLSpec = { 66 | .appURL=(CFURLRef)appURL, 67 | .itemURLs=(CFArrayRef)itemURLs, 68 | .launchFlags=launchFlags, 69 | }; 70 | CFURLRef *launchedAppOut = NULL; 71 | OSStatus status = LSOpenFromURLSpec(&launchURLSpec, launchedAppOut); 72 | if (status) { 73 | if (error) { 74 | *error = [NSError errorWithDomain:NSOSStatusErrorDomain 75 | code:status 76 | userInfo:nil]; 77 | } 78 | return NO; 79 | } else { 80 | return YES; 81 | } 82 | } 83 | 84 | - (NSArray *)URLsForBundleIdentifier:(NSString *)bundleIdentifier 85 | error:(NSError **)error 86 | { 87 | NSError *localError = nil; 88 | NSArray *urls = (NSArray *)LSCopyApplicationURLsForBundleIdentifier((CFStringRef)bundleIdentifier, (CFErrorRef *)&localError); 89 | [urls autorelease]; 90 | [localError autorelease]; 91 | if (urls) { 92 | return urls; 93 | } else { 94 | if (localError && kLSApplicationNotFoundErr == localError.code) { 95 | return @[]; 96 | } else { 97 | if (error) *error = localError; 98 | return nil; 99 | } 100 | } 101 | } 102 | 103 | @end 104 | -------------------------------------------------------------------------------- /openman/LaunchServicesFake.h: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchServicesFake.h 3 | // openmanTests 4 | // 5 | // Created by Don McCaughey on 2/14/18. 6 | // 7 | 8 | #import "LaunchServices.h" 9 | 10 | 11 | @class Application; 12 | 13 | 14 | @interface LaunchServicesFake : NSObject 15 | 16 | @property (retain) NSError *errorOut; 17 | @property (copy) NSArray *return_applications; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /openman/LaunchServicesFake.m: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchServicesFake.m 3 | // openmanTests 4 | // 5 | // Created by Don McCaughey on 2/14/18. 6 | // 7 | 8 | #import "LaunchServicesFake.h" 9 | 10 | 11 | @implementation LaunchServicesFake 12 | 13 | - (void)dealloc 14 | { 15 | [_errorOut release]; 16 | [_return_applications release]; 17 | [super dealloc]; 18 | } 19 | 20 | - (NSArray *)applicationsForBundleIdentifier:(NSString *)bundleIdentifier 21 | error:(NSError **)error 22 | { 23 | if (error) *error = [[_errorOut retain] autorelease]; 24 | return [[_return_applications retain] autorelease]; 25 | } 26 | 27 | - (BOOL)openItemURLs:(NSArray *)itemURLs 28 | inApplication:(Application *)application 29 | error:(NSError **)error 30 | { 31 | [self doesNotRecognizeSelector:_cmd]; 32 | return NO; 33 | } 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /openman/LaunchServicesTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchServicesTests.m 3 | // openmanTests 4 | // 5 | // Created by Don McCaughey on 2/14/18. 6 | // 7 | 8 | #import 9 | #import "LaunchServices.h" 10 | #import "Application.h" 11 | #import "Version.h" 12 | 13 | 14 | @interface LaunchServicesTests : XCTestCase 15 | @end 16 | 17 | 18 | @implementation LaunchServicesTests 19 | 20 | #pragma mark - applicationsForBundleIdentifier:error: 21 | 22 | - (void)testApplicationsForBundleIdentifierError 23 | { 24 | LaunchServices *launchServices = [[LaunchServices new] autorelease]; 25 | NSError *error = nil; 26 | NSArray *applications = [launchServices applicationsForBundleIdentifier:@"com.apple.dt.Xcode" 27 | error:&error]; 28 | XCTAssertTrue(applications.count > 0); 29 | XCTAssertNil(error); 30 | 31 | for (Application *application in applications) { 32 | XCTAssertEqualObjects(@"com.apple.dt.Xcode", application.bundleIdentifier); 33 | XCTAssertNotNil(application.url); 34 | XCTAssertTrue(application.version.major > 0); 35 | } 36 | } 37 | 38 | - (void)testApplicationsForBundleIdentifierError_when_no_apps_found 39 | { 40 | LaunchServices *launchServices = [[LaunchServices new] autorelease]; 41 | NSError *error = nil; 42 | NSArray *applications = [launchServices applicationsForBundleIdentifier:@"com.example.NotAnApp" 43 | error:&error]; 44 | XCTAssertEqual(0, applications.count); 45 | XCTAssertNil(error); 46 | } 47 | 48 | - (void)testApplicationsForBundleIdentifierError_when_invalid_bundle_ID 49 | { 50 | LaunchServices *launchServices = [[LaunchServices new] autorelease]; 51 | NSError *error = nil; 52 | NSArray *applications = [launchServices applicationsForBundleIdentifier:nil 53 | error:&error]; 54 | XCTAssertNil(applications); 55 | XCTAssertNotNil(error); 56 | } 57 | 58 | #pragma mark - bundlesForBundleIdentifier:error: 59 | 60 | - (void)testBundlesForBundleIdentifierError 61 | { 62 | LaunchServices *launchServices = [[LaunchServices new] autorelease]; 63 | NSError *error = nil; 64 | NSArray *bundles = [launchServices bundlesForBundleIdentifier:@"com.apple.dt.Xcode" 65 | error:&error]; 66 | XCTAssertTrue(bundles.count > 0); 67 | XCTAssertNil(error); 68 | } 69 | 70 | - (void)testBundlesForBundleIdentifierError_when_no_apps_found 71 | { 72 | LaunchServices *launchServices = [[LaunchServices new] autorelease]; 73 | NSError *error = nil; 74 | NSArray *bundles = [launchServices bundlesForBundleIdentifier:@"com.example.NotAnApp" 75 | error:&error]; 76 | XCTAssertEqual(0, bundles.count); 77 | XCTAssertNil(error); 78 | } 79 | 80 | - (void)testBundlesForBundleIdentifierError_when_invalid_bundle_ID 81 | { 82 | LaunchServices *launchServices = [[LaunchServices new] autorelease]; 83 | NSError *error = nil; 84 | NSArray *bundles = [launchServices bundlesForBundleIdentifier:nil 85 | error:&error]; 86 | XCTAssertNil(bundles); 87 | XCTAssertNotNil(error); 88 | } 89 | 90 | #pragma mark - URLsForBundleIdentifier:error: 91 | 92 | - (void)testURLsForBundleIdentifierError 93 | { 94 | LaunchServices *launchServices = [[LaunchServices new] autorelease]; 95 | NSError *error = nil; 96 | NSArray *urls = [launchServices URLsForBundleIdentifier:@"com.apple.dt.Xcode" 97 | error:&error]; 98 | XCTAssertTrue(urls.count > 0); 99 | XCTAssertNil(error); 100 | } 101 | 102 | - (void)testURLsForBundleIdentifierError_when_no_apps_found 103 | { 104 | LaunchServices *launchServices = [[LaunchServices new] autorelease]; 105 | NSError *error = nil; 106 | NSArray *urls = [launchServices URLsForBundleIdentifier:@"com.example.NotAnApp" 107 | error:&error]; 108 | XCTAssertNotNil(urls); 109 | XCTAssertEqual(0, urls.count); 110 | XCTAssertNil(error); 111 | } 112 | 113 | - (void)testURLsForBundleIdentifierError_when_invalid_bundle_ID 114 | { 115 | LaunchServices *launchServices = [[LaunchServices new] autorelease]; 116 | NSError *error = nil; 117 | NSArray *urls = [launchServices URLsForBundleIdentifier:nil 118 | error:&error]; 119 | XCTAssertNil(urls); 120 | XCTAssertNotNil(error); 121 | } 122 | 123 | @end 124 | -------------------------------------------------------------------------------- /openman/Version.h: -------------------------------------------------------------------------------- 1 | // 2 | // Version.h 3 | // openman 4 | // 5 | // Created by Don McCaughey on 2/13/18. 6 | // 7 | 8 | #import 9 | 10 | 11 | @interface Version : NSObject 12 | 13 | @property NSUInteger major; 14 | @property NSUInteger minor; 15 | @property NSUInteger patch; 16 | 17 | - (instancetype)initWithMajor:(NSUInteger)major 18 | minor:(NSUInteger)minor 19 | patch:(NSUInteger)patch; 20 | 21 | - (instancetype)initWithVersion:(NSString *)version; 22 | 23 | - (NSComparisonResult)compare:(Version *)version; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /openman/Version.m: -------------------------------------------------------------------------------- 1 | // 2 | // Version.m 3 | // openman 4 | // 5 | // Created by Don McCaughey on 2/13/18. 6 | // 7 | 8 | #import "Version.h" 9 | 10 | 11 | @implementation Version 12 | 13 | - (instancetype)init 14 | { 15 | return [self initWithMajor:0 16 | minor:0 17 | patch:0]; 18 | } 19 | 20 | - (instancetype)initWithMajor:(NSUInteger)major 21 | minor:(NSUInteger)minor 22 | patch:(NSUInteger)patch 23 | { 24 | self = [super init]; 25 | if (self) { 26 | _major = major; 27 | _minor = minor; 28 | _patch = patch; 29 | } 30 | return self; 31 | } 32 | 33 | - (instancetype)initWithVersion:(NSString *)version 34 | { 35 | if (!version.length) return nil; 36 | 37 | NSInteger major = 0; 38 | NSInteger minor = 0; 39 | NSInteger patch = 0; 40 | 41 | static NSString *pattern = 42 | @"^" // (anchored) 43 | "(\\d+)" // major number 44 | "(?:" // (non-capturing group) 45 | "\\." // dot 46 | "(\\d+)" // minor number 47 | "(?:" // (non-capturing group) 48 | "\\." // dot 49 | "(\\d+)" // patch number 50 | ")?" // (optional) 51 | ")?" // (optional) 52 | "$" // (anchored) 53 | ; 54 | static NSRegularExpression *regex = nil; 55 | static dispatch_once_t onceToken; 56 | dispatch_once(&onceToken, ^{ 57 | regex = [NSRegularExpression regularExpressionWithPattern:pattern 58 | options:0 59 | error:nil]; 60 | [regex retain]; 61 | NSAssert(regex, @"Expected to initialize regular expression"); 62 | }); 63 | 64 | NSRange range = NSMakeRange(0, version.length); 65 | NSTextCheckingResult *match = [regex firstMatchInString:version 66 | options:NSMatchingAnchored 67 | range:range]; 68 | if (!match) return nil; 69 | 70 | NSString *majorString = [version substringWithRange:[match rangeAtIndex:1]]; 71 | major = majorString.integerValue; 72 | 73 | if ([match rangeAtIndex:2].length) { 74 | NSString *minorString = [version substringWithRange:[match rangeAtIndex:2]]; 75 | minor = minorString.integerValue; 76 | } 77 | 78 | if ([match rangeAtIndex:3].length) { 79 | NSString *patchString = [version substringWithRange:[match rangeAtIndex:3]]; 80 | patch = patchString.integerValue; 81 | } 82 | 83 | return [self initWithMajor:major 84 | minor:minor 85 | patch:patch]; 86 | } 87 | 88 | - (NSString *)debugDescription 89 | { 90 | return [NSString stringWithFormat:@"v%lu.%lu.%lu", 91 | (unsigned long)_major, (unsigned long)_minor, (unsigned long)_patch]; 92 | } 93 | 94 | - (NSString *)description 95 | { 96 | if (_patch) { 97 | return [NSString stringWithFormat:@"%lu.%lu.%lu", 98 | (unsigned long)_major, (unsigned long)_minor, (unsigned long)_patch]; 99 | } else { 100 | return [NSString stringWithFormat:@"%lu.%lu", 101 | (unsigned long)_major, (unsigned long)_minor]; 102 | } 103 | } 104 | 105 | - (NSUInteger)hash 106 | { 107 | // From https://stackoverflow.com/a/2816747 108 | NSUInteger const prime = 92821; 109 | NSUInteger hash = prime + _major; 110 | hash = prime * hash + _minor; 111 | hash = prime * hash + _patch; 112 | return hash; 113 | } 114 | 115 | - (BOOL)isEqual:(id)object 116 | { 117 | if (self == object) return YES; 118 | if (![object isKindOfClass:[Version class]]) return NO; 119 | return NSOrderedSame == [self compare:object]; 120 | } 121 | 122 | - (NSComparisonResult)compare:(Version *)version 123 | { 124 | if (!version) { 125 | [NSException raise:NSInvalidArgumentException 126 | format:@"version cannot be nil"]; 127 | } 128 | if (_major != version->_major) { 129 | return (_major < version->_major) ? NSOrderedAscending : NSOrderedDescending; 130 | } 131 | if (_minor != version->_minor) { 132 | return (_minor < version->_minor) ? NSOrderedAscending : NSOrderedDescending; 133 | } 134 | if (_patch != version->_patch) { 135 | return (_patch < version->_patch) ? NSOrderedAscending : NSOrderedDescending; 136 | } 137 | return NSOrderedSame; 138 | } 139 | 140 | @end 141 | -------------------------------------------------------------------------------- /openman/VersionTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // VersionTests.m 3 | // openmanTests 4 | // 5 | // Created by Don McCaughey on 2/13/18. 6 | // 7 | 8 | #import 9 | #import "Version.h" 10 | 11 | 12 | @interface VersionTests : XCTestCase 13 | @end 14 | 15 | 16 | @implementation VersionTests 17 | 18 | - (void)testInit 19 | { 20 | Version *version = [[Version new] autorelease]; 21 | XCTAssertEqual(0, version.major); 22 | XCTAssertEqual(0, version.minor); 23 | XCTAssertEqual(0, version.patch); 24 | XCTAssertEqualObjects(@"0.0", version.description); 25 | XCTAssertEqualObjects(@"v0.0.0", version.debugDescription); 26 | } 27 | 28 | - (void)testInitWithMajorMinorPatch 29 | { 30 | Version *version = [[Version alloc] initWithMajor:1 31 | minor:2 32 | patch:3]; 33 | [version autorelease]; 34 | XCTAssertEqual(1, version.major); 35 | XCTAssertEqual(2, version.minor); 36 | XCTAssertEqual(3, version.patch); 37 | XCTAssertEqualObjects(@"1.2.3", version.description); 38 | XCTAssertEqualObjects(@"v1.2.3", version.debugDescription); 39 | } 40 | 41 | - (void)testInitWithMajorMinorPatch_for_patch_zero 42 | { 43 | Version *version = [[Version alloc] initWithMajor:11 44 | minor:12 45 | patch:0]; 46 | [version autorelease]; 47 | XCTAssertEqual(11, version.major); 48 | XCTAssertEqual(12, version.minor); 49 | XCTAssertEqual(0, version.patch); 50 | XCTAssertEqualObjects(@"11.12", version.description); 51 | XCTAssertEqualObjects(@"v11.12.0", version.debugDescription); 52 | } 53 | 54 | - (void)testInitWithVersion_when_nil 55 | { 56 | Version *version = [[Version alloc] initWithVersion:nil]; 57 | XCTAssertNil(version); 58 | } 59 | 60 | - (void)testInitWithVersion_when_empty 61 | { 62 | Version *version = [[Version alloc] initWithVersion:@""]; 63 | XCTAssertNil(version); 64 | } 65 | 66 | - (void)testInitWithVersion_when_not_a_version 67 | { 68 | Version *version = [[Version alloc] initWithVersion:@" "]; 69 | XCTAssertNil(version); 70 | 71 | version = [[Version alloc] initWithVersion:@"foo"]; 72 | XCTAssertNil(version); 73 | 74 | version = [[Version alloc] initWithVersion:@"1.foo"]; 75 | XCTAssertNil(version); 76 | 77 | version = [[Version alloc] initWithVersion:@"1.2.foo"]; 78 | XCTAssertNil(version); 79 | 80 | version = [[Version alloc] initWithVersion:@"1.2.3a"]; 81 | XCTAssertNil(version); 82 | 83 | version = [[Version alloc] initWithVersion:@"1.2.3.4"]; 84 | XCTAssertNil(version); 85 | 86 | version = [[Version alloc] initWithVersion:@".2.3.4"]; 87 | XCTAssertNil(version); 88 | 89 | version = [[Version alloc] initWithVersion:@"-1.2.3"]; 90 | XCTAssertNil(version); 91 | 92 | version = [[Version alloc] initWithVersion:@"1.-2.3"]; 93 | XCTAssertNil(version); 94 | 95 | version = [[Version alloc] initWithVersion:@"1.2.-3"]; 96 | XCTAssertNil(version); 97 | 98 | version = [[Version alloc] initWithVersion:@"1. 2"]; 99 | XCTAssertNil(version); 100 | 101 | version = [[Version alloc] initWithVersion:@"1.2 .3"]; 102 | XCTAssertNil(version); 103 | 104 | version = [[Version alloc] initWithVersion:@" 1.2.3"]; 105 | XCTAssertNil(version); 106 | 107 | version = [[Version alloc] initWithVersion:@"1.2.3 "]; 108 | XCTAssertNil(version); 109 | } 110 | 111 | - (void)testInitWithVersion_with_one_part 112 | { 113 | Version *version = [[[Version alloc] initWithVersion:@"8"] autorelease]; 114 | XCTAssertEqual(8, version.major); 115 | XCTAssertEqual(0, version.minor); 116 | XCTAssertEqual(0, version.patch); 117 | XCTAssertEqualObjects(@"8.0", version.description); 118 | XCTAssertEqualObjects(@"v8.0.0", version.debugDescription); 119 | } 120 | 121 | - (void)testInitWithVersion_with_two_parts 122 | { 123 | Version *version = [[[Version alloc] initWithVersion:@"4.5"] autorelease]; 124 | XCTAssertEqual(4, version.major); 125 | XCTAssertEqual(5, version.minor); 126 | XCTAssertEqual(0, version.patch); 127 | XCTAssertEqualObjects(@"4.5", version.description); 128 | XCTAssertEqualObjects(@"v4.5.0", version.debugDescription); 129 | } 130 | 131 | - (void)testInitWithVersion_with_three_parts 132 | { 133 | Version *version = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 134 | XCTAssertEqual(1, version.major); 135 | XCTAssertEqual(2, version.minor); 136 | XCTAssertEqual(3, version.patch); 137 | XCTAssertEqualObjects(@"1.2.3", version.description); 138 | XCTAssertEqualObjects(@"v1.2.3", version.debugDescription); 139 | } 140 | 141 | - (void)testInitWithVersion_with_leading_zeros 142 | { 143 | Version *version = [[[Version alloc] initWithVersion:@"01.002.0003"] autorelease]; 144 | XCTAssertEqual(1, version.major); 145 | XCTAssertEqual(2, version.minor); 146 | XCTAssertEqual(3, version.patch); 147 | XCTAssertEqualObjects(@"1.2.3", version.description); 148 | XCTAssertEqualObjects(@"v1.2.3", version.debugDescription); 149 | } 150 | 151 | - (void)testEqualTo_and_hash 152 | { 153 | Version *version1 = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 154 | Version *version2 = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 155 | 156 | XCTAssertFalse([version1 isEqual:nil]); 157 | XCTAssertEqualObjects(version1, version1); 158 | 159 | XCTAssertEqual(version1.hash, version2.hash); 160 | XCTAssertEqualObjects(version1, version2); 161 | XCTAssertEqualObjects(version2, version1); 162 | 163 | version1 = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 164 | version2 = [[[Version alloc] initWithVersion:@"1.2.4"] autorelease]; 165 | XCTAssertNotEqualObjects(version1, version2); 166 | XCTAssertNotEqualObjects(version2, version1); 167 | 168 | version1 = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 169 | version2 = [[[Version alloc] initWithVersion:@"1.5.3"] autorelease]; 170 | XCTAssertNotEqualObjects(version1, version2); 171 | XCTAssertNotEqualObjects(version2, version1); 172 | 173 | version1 = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 174 | version2 = [[[Version alloc] initWithVersion:@"6.2.3"] autorelease]; 175 | XCTAssertNotEqualObjects(version1, version2); 176 | XCTAssertNotEqualObjects(version2, version1); 177 | } 178 | 179 | - (void)testCompare 180 | { 181 | Version *version1 = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 182 | Version *version2 = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 183 | 184 | XCTAssertThrows([version1 compare:nil]); 185 | 186 | XCTAssertEqual(NSOrderedSame, [version1 compare:version2]); 187 | XCTAssertEqual(NSOrderedSame, [version2 compare:version1]); 188 | 189 | version1 = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 190 | version2 = [[[Version alloc] initWithVersion:@"1.2.4"] autorelease]; 191 | XCTAssertEqual(NSOrderedAscending, [version1 compare:version2]); 192 | XCTAssertEqual(NSOrderedDescending, [version2 compare:version1]); 193 | 194 | version1 = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 195 | version2 = [[[Version alloc] initWithVersion:@"1.3.3"] autorelease]; 196 | XCTAssertEqual(NSOrderedAscending, [version1 compare:version2]); 197 | XCTAssertEqual(NSOrderedDescending, [version2 compare:version1]); 198 | 199 | version1 = [[[Version alloc] initWithVersion:@"1.2.3"] autorelease]; 200 | version2 = [[[Version alloc] initWithVersion:@"2.2.3"] autorelease]; 201 | XCTAssertEqual(NSOrderedAscending, [version1 compare:version2]); 202 | XCTAssertEqual(NSOrderedDescending, [version2 compare:version1]); 203 | } 204 | 205 | @end 206 | -------------------------------------------------------------------------------- /openman/openman.1: -------------------------------------------------------------------------------- 1 | .TH OPENMAN 1 2 | .SH NAME 3 | openman \- display manual pages in ManOpen.app 4 | .SH SYNOPSIS 5 | .B openman 6 | [ 7 | .BI \-kb 8 | ] 9 | [ 10 | .BI \-M 11 | .I path 12 | ] 13 | [ 14 | .BI \-m 15 | .I path 16 | ] 17 | [ 18 | .BI \-f 19 | .I file 20 | ] 21 | [ 22 | .I section 23 | ] 24 | [ 25 | .I name ... 26 | ] 27 | 28 | .SH DESCRIPTION 29 | .I Openman 30 | is a command-line utility to open Unix man pages in ManOpen.app. The 31 | syntax is generally the same as the man(1) command, with an additional 32 | option to directly open files. 33 | .PP 34 | .I Openman 35 | will open a page in ManOpen.app for every title given. If a section 36 | specifier is given, 37 | .I openman 38 | looks in that section of the manual for the given 39 | .I titles. 40 | .I Section 41 | is typically an Arabic section number (``1'' for user commands, 42 | ``8'' for administrative commands, etc), but it can be a named 43 | section as well. If 44 | .I section 45 | is omitted, 46 | .I openman 47 | searches all sections of the manual, giving preference to commands 48 | over subroutines in system libraries, and printing the first section 49 | it finds, if any. 50 | .PP 51 | If the 52 | .B \-k 53 | flag is specified, then apropos(1) mode is used, with each given title 54 | used as an Apropos lookup in ManOpen.app instead of being opened as 55 | an individual page. 56 | .PP 57 | Normally, ManOpen.app is brought forward to be the active application 58 | when messaged by openman (meaning Terminal.app will no longer be the 59 | active application). If the 60 | .B \-b 61 | flag is specfied, ManOpen.app not be forcibly be made active (i.e. 62 | will stay in the background). 63 | .PP 64 | The man search path can be specified with the 65 | .B \-M 66 | or 67 | .B \-m 68 | flag. The search path is a colon (`:') separated list of directories 69 | in which manual subdirectories may be found; e.g. ``/usr/local/man:/usr/share/man''. 70 | .hw MANPATH 71 | If a search path is not supplied, the value of the environment variable 72 | `MANPATH' is used for the search path. If that isn't set, ManOpen's 73 | man path (as specified in the application's preferences) is used. 74 | .PP 75 | A file can be directly specified with the 76 | .B \-f 77 | flag. If it's an nroff source file, ManOpen's "Nroff command" (as set in 78 | the applications's preferences) will be used to process the file, 79 | otherwise ManOpen will open it directly. The argument can be a full or 80 | relative path. 81 | .PP 82 | If you use the tcsh shell, you can set openman's completion settings to 83 | be similar to man(1)'s, which causes it to complete using command names. 84 | Add the following to your ~/.cshrc or ~/.tchrc: 85 | .IP 86 | complete openman 'n/-M/d/' 'p/*/c/' 87 | .PP 88 | .SH "SEE ALSO" 89 | ManOpen.app, man(1), apropos(1), whereis(1) 90 | .SH AUTHOR 91 | Carl Lindberg 92 | .PP 93 | Please send any bug reports/suggestions. 94 | -------------------------------------------------------------------------------- /openman/openman.m: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | #import 4 | #import // for getopt() 5 | #import // for isdigit() 6 | #import "Application.h" 7 | #import "LaunchServices.h" 8 | #import "ManOpenURLComponents.h" 9 | #import "SystemType.h" 10 | 11 | 12 | static NSString *MakeNSStringFromPath(const char *filename) 13 | { 14 | NSFileManager *manager = [NSFileManager defaultManager]; 15 | return [manager stringWithFileSystemRepresentation:filename length:strlen(filename)]; 16 | } 17 | 18 | static NSString *MakeAbsolutePath(const char *filename) 19 | { 20 | NSString *currFile = MakeNSStringFromPath(filename); 21 | 22 | if (![currFile isAbsolutePath]) 23 | { 24 | currFile = [[[NSFileManager defaultManager] currentDirectoryPath] 25 | stringByAppendingPathComponent:currFile]; 26 | } 27 | 28 | return currFile; 29 | } 30 | 31 | static void usage(const char *progname) 32 | { 33 | fprintf(stderr,"%s: [-bk] [-M path] [-f file] [section] [name ...]\n", progname); 34 | } 35 | 36 | int main (int argc, char * const *argv) 37 | { 38 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 39 | NSString *manPath = nil; 40 | NSString *section = nil; 41 | NSMutableArray *files = [NSMutableArray array]; 42 | BOOL aproposMode = NO; 43 | BOOL forceToFront = YES; 44 | int argIndex; 45 | NSUInteger fileIndex; 46 | char c; 47 | 48 | while ((c = getopt(argc,argv,"hbm:M:f:kaCcw")) != EOF) 49 | { 50 | switch(c) 51 | { 52 | case 'm': 53 | case 'M': 54 | manPath = MakeNSStringFromPath(optarg); 55 | break; 56 | case 'f': 57 | [files addObject:MakeAbsolutePath(optarg)]; 58 | break; 59 | case 'b': 60 | forceToFront = NO; 61 | break; 62 | case 'k': 63 | aproposMode = YES; 64 | break; 65 | case 'a': 66 | case 'C': 67 | case 'c': 68 | case 'w': 69 | // MacOS X man(1) options; no-op here. 70 | break; 71 | case 'h': 72 | case '?': 73 | default: 74 | usage(argv[0]); 75 | [pool release]; 76 | exit(0); 77 | } 78 | } 79 | 80 | if (optind >= argc && [files count] <= 0) 81 | { 82 | usage(argv[0]); 83 | [pool release]; 84 | exit(0); 85 | } 86 | 87 | if (optind < argc && !aproposMode) 88 | { 89 | NSString *tmp = MakeNSStringFromPath(argv[optind]); 90 | 91 | if (isdigit(argv[optind][0]) || 92 | /* These are configurable in /etc/man.conf; these are just the default strings. Hm, they are invalid as of Panther. */ 93 | [tmp isEqualToString:@"system"] || 94 | [tmp isEqualToString:@"commands"] || 95 | [tmp isEqualToString:@"syscalls"] || 96 | [tmp isEqualToString:@"libc"] || 97 | [tmp isEqualToString:@"special"] || 98 | [tmp isEqualToString:@"files"] || 99 | [tmp isEqualToString:@"games"] || 100 | [tmp isEqualToString:@"miscellaneous"] || 101 | [tmp isEqualToString:@"misc"] || 102 | [tmp isEqualToString:@"admin"] || 103 | [tmp isEqualToString:@"n"] || // Tcl pages on >= Panther 104 | [tmp isEqualToString:@"local"]) 105 | { 106 | section = tmp; 107 | optind++; 108 | } 109 | } 110 | 111 | if (optind >= argc) 112 | { 113 | if ([section length] > 0) 114 | { 115 | /* MacOS X assumes it's a man page name */ 116 | section = nil; 117 | optind--; 118 | } 119 | 120 | if (optind >= argc && [files count] <= 0) 121 | { 122 | [pool release]; 123 | exit(0); 124 | } 125 | } 126 | 127 | id launchServices = [[LaunchServices new] autorelease]; 128 | NSError *error = nil; 129 | Application *latestVersion = [Application latestVersionWithLaunchServices:launchServices 130 | bundleIdentifier:@"cc.donm.ManOpen" 131 | error:&error]; 132 | if (!latestVersion) { 133 | if (error) { 134 | fprintf(stderr, "%s:%li: %s", 135 | error.domain.UTF8String, (long)error.code, 136 | error.localizedDescription.UTF8String); 137 | } else { 138 | fprintf(stderr, "Unable to locate ManOpen application\n"); 139 | } 140 | [pool release]; 141 | exit(1); 142 | } 143 | 144 | NSMutableArray *itemURLs = [[NSMutableArray new] autorelease]; 145 | 146 | for (fileIndex=0; fileIndex<[files count]; fileIndex++) 147 | { 148 | ManOpenURLComponents *components = [[ManOpenURLComponents alloc] initWithFilePath:[files objectAtIndex:fileIndex] 149 | isBackground:!forceToFront]; 150 | [components autorelease]; 151 | [itemURLs addObject:components.url]; 152 | } 153 | 154 | if (manPath == nil && getenv("MANPATH") != NULL) 155 | manPath = MakeNSStringFromPath(getenv("MANPATH")); 156 | 157 | for (argIndex = optind; argIndex < argc; argIndex++) 158 | { 159 | NSString *currFile = MakeNSStringFromPath(argv[argIndex]); 160 | if (aproposMode) { 161 | ManOpenURLComponents *components = [[ManOpenURLComponents alloc] initWithAproposKeyword:currFile 162 | manPath:manPath 163 | isBackground:!forceToFront]; 164 | [components autorelease]; 165 | [itemURLs addObject:components.url]; 166 | } else { 167 | ManOpenURLComponents *components = [[ManOpenURLComponents alloc] initWithSection:section 168 | name:currFile 169 | manPath:manPath 170 | isBackground:!forceToFront]; 171 | [components autorelease]; 172 | [itemURLs addObject:components.url]; 173 | } 174 | } 175 | 176 | BOOL success = [launchServices openItemURLs:itemURLs 177 | inApplication:latestVersion 178 | error:&error]; 179 | if (!success) { 180 | if (error) { 181 | fprintf(stderr, "%s:%li: %s", 182 | error.domain.UTF8String, (long)error.code, 183 | error.localizedDescription.UTF8String); 184 | } else { 185 | fprintf(stderr, "Unable to launch ManOpen application\n"); 186 | } 187 | [pool release]; 188 | exit(1); 189 | } 190 | 191 | [pool release]; 192 | exit(0); // insure the process exit status is 0 193 | return 0; // ...and make main fit the ANSI spec. 194 | } 195 | -------------------------------------------------------------------------------- /openman/openmanTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /scripts/GetURLForFile.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/scripts/GetURLForFile.scpt -------------------------------------------------------------------------------- /scripts/GetURLForManopen.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/scripts/GetURLForManopen.scpt -------------------------------------------------------------------------------- /scripts/GetURLForManopenApropos.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/scripts/GetURLForManopenApropos.scpt -------------------------------------------------------------------------------- /scripts/GetURLForManopenFilePath.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/scripts/GetURLForManopenFilePath.scpt -------------------------------------------------------------------------------- /scripts/GetURLForXManPage.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/scripts/GetURLForXManPage.scpt -------------------------------------------------------------------------------- /scripts/GetURLForXManPageApropos.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/scripts/GetURLForXManPageApropos.scpt -------------------------------------------------------------------------------- /scripts/OpenFile.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccaughey/ManOpen/01937a5226f072a7915f76931c373f095254ebb9/scripts/OpenFile.scpt --------------------------------------------------------------------------------