From 645c83726269d6cdff2076c45d504cf18b2a74aa Mon Sep 17 00:00:00 2001 From: Allan Odgaard Date: Fri, 14 Mar 2014 13:56:57 +0700 Subject: [PATCH] Skeleton commit window server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows the commit command line tool to open a window as “native”. We use distributed objects for talking to TextMate and getting a response. For the response, we release the connection in the next iteration of the event loop and then gracefully exit the program. Though it’s not clear if this is enough time for distributed objects to reply the client (if not, an exception is thrown in the client about “connection disappeared while waiting for a reply”). --- .../TextMate/resources/Default.tmProperties | 11 +- Applications/TextMate/src/AppController.mm | 3 + Applications/TextMate/target | 3 +- Applications/commit/src/commit.mm | 85 ++++++++ Applications/commit/target | 1 + Frameworks/CommitWindow/src/CommitWindow.h | 21 ++ Frameworks/CommitWindow/src/CommitWindow.mm | 192 ++++++++++++++++++ Frameworks/CommitWindow/target | 4 + 8 files changed, 314 insertions(+), 6 deletions(-) create mode 100644 Applications/commit/src/commit.mm create mode 100644 Applications/commit/target create mode 100644 Frameworks/CommitWindow/src/CommitWindow.h create mode 100644 Frameworks/CommitWindow/src/CommitWindow.mm create mode 100644 Frameworks/CommitWindow/target diff --git a/Applications/TextMate/resources/Default.tmProperties b/Applications/TextMate/resources/Default.tmProperties index 3fe325ef..ba458b93 100644 --- a/Applications/TextMate/resources/Default.tmProperties +++ b/Applications/TextMate/resources/Default.tmProperties @@ -11,11 +11,12 @@ windowTitleSCM = '${TM_SCM_BRANCH:+ ($TM_SCM_NAME: $TM_SCM_BRANCH)}' windowTitleProject = '${projectDirectory:+ — ${projectDirectory/^.*\///}}' windowTitle = '$TM_DISPLAYNAME$windowTitleProject$windowTitleSCM' -LANG = "en_US.UTF-8" -LC_CTYPE = "en_US.UTF-8" -TM_APP_PATH = "${CWD/\/Contents\/Resources$//}" -TM_MATE = "$CWD/mate" -TM_QUERY = "$CWD/tm_query" +LANG = "en_US.UTF-8" +LC_CTYPE = "en_US.UTF-8" +TM_APP_PATH = "${CWD/\/Contents\/Resources$//}" +TM_MATE = "$CWD/mate" +TM_QUERY = "$CWD/tm_query" +TM_SCM_COMMIT_WINDOW = "$CWD/commit" [ attr.untitled ] fileType = text.plain diff --git a/Applications/TextMate/src/AppController.mm b/Applications/TextMate/src/AppController.mm index 754ecac2..524609f0 100644 --- a/Applications/TextMate/src/AppController.mm +++ b/Applications/TextMate/src/AppController.mm @@ -8,6 +8,7 @@ #import #import #import +#import #import #import #import @@ -297,6 +298,8 @@ BOOL HasDocumentWindow (NSArray* windows) [[CrashReporter sharedInstance] applicationDidFinishLaunching:aNotification]; [[CrashReporter sharedInstance] postNewCrashReportsToURLString:REST_API @"/crashes"]; + [OakCommitWindowServer sharedInstance]; // Setup server + self.didFinishLaunching = YES; } diff --git a/Applications/TextMate/target b/Applications/TextMate/target index b2d377d5..2d557514 100644 --- a/Applications/TextMate/target +++ b/Applications/TextMate/target @@ -1,11 +1,12 @@ SOURCES = src/*.{cc,mm} -CP_Resources = resources/* icons/*.icns about/* @PrivilegedTool @mate @tm_query +CP_Resources = resources/* icons/*.icns about/* @PrivilegedTool @mate @tm_query @commit CP_SharedSupport = support/* CP_PlugIns = @Dialog @Dialog2 CP_Library/QuickLook = @TextMateQL FLAGS += -DREST_API='@"$rest_api"' LINK += bundles cf command document editor io network ns plist settings text kvdb LINK += BundleMenu BundleEditor BundlesManager CrashReporter DocumentWindow Find HTMLOutputWindow OakAppKit OakFilterList OakFoundation OakSystem OakTextView Preferences SoftwareUpdate updater license +LINK += CommitWindow FRAMEWORKS = Cocoa HTML_HEADER = templates/header.html HTML_FOOTER = templates/footer.html diff --git a/Applications/commit/src/commit.mm b/Applications/commit/src/commit.mm new file mode 100644 index 00000000..02841e13 --- /dev/null +++ b/Applications/commit/src/commit.mm @@ -0,0 +1,85 @@ +#include +#include + +static double const AppVersion = 1.0; +static size_t const AppRevision = APP_REVISION; + +@interface OakCommitWindowClient : NSObject +@property (nonatomic) NSString* portName; +@property (nonatomic) NSConnection* connection; +@property (nonatomic) NSInteger returnCode; +@end + +@implementation OakCommitWindowClient +- (id)init +{ + if(self = [super init]) + { + _portName = [NSString stringWithFormat:@"com.macromates.commit-window-client.%d", getpid()]; + _connection = [NSConnection new]; + + [_connection setRootObject:self]; + if([_connection registerName:_portName] == NO) + { + fprintf(stderr, "%s: failed vending object as ‘%s’\n", getprogname(), [_portName UTF8String]); + return nil; + } + } + return self; +} + +- (void)connectFromServerWithOptions:(NSDictionary*)someOptions +{ + if(NSString* err = someOptions[kOakCommitWindowStandardError]) + fprintf(stderr, "%s", [err UTF8String]); + + if(NSString* out = someOptions[kOakCommitWindowStandardOutput]) + fprintf(stdout, "%s", [out UTF8String]); + + _returnCode = [someOptions[kOakCommitWindowReturnCode] intValue]; + + // Tear down the connection in next event loop iteration. + // This should allow the sender to get a reply. + [self performSelector:@selector(setConnection:) withObject:nil afterDelay:0]; +} +@end + +int main (int argc, char* argv[]) +{ + if(argc == 2 && (strcmp(argv[1], "-v") == 0 || strcmp(argv[1], "--version") == 0)) + { + fprintf(stderr, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision); + return 0; + } + + @autoreleasepool { + if(OakCommitWindowClient* client = [[OakCommitWindowClient alloc] init]) + { + NSMutableArray* arg = [NSMutableArray array]; + for(size_t i = 0; i < argc; ++i) + [arg addObject:[NSString stringWithUTF8String:argv[i]]]; + + NSDictionary* plist = @{ + kOakCommitWindowClientPortName : client.portName, + kOakCommitWindowArguments : arg, + kOakCommitWindowEnvironment : [[NSProcessInfo processInfo] environment], + }; + + if(id proxy = [NSConnection rootProxyForConnectionWithRegisteredName:kOakCommitWindowServerConnectionName host:nil]) + { + [proxy setProtocolForProxy:@protocol(OakCommitWindowServerProtocol)]; + [proxy connectFromClientWithOptions:plist]; + + while(client.connection) + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + + return client.returnCode; + } + else + { + fprintf(stderr, "%s: failed connecting to ‘%s’\n", getprogname(), [kOakCommitWindowServerConnectionName UTF8String]); + } + } + } + return 0; +} diff --git a/Applications/commit/target b/Applications/commit/target new file mode 100644 index 00000000..3a5723fc --- /dev/null +++ b/Applications/commit/target @@ -0,0 +1 @@ +SOURCES = src/*.mm diff --git a/Frameworks/CommitWindow/src/CommitWindow.h b/Frameworks/CommitWindow/src/CommitWindow.h new file mode 100644 index 00000000..e2541ddd --- /dev/null +++ b/Frameworks/CommitWindow/src/CommitWindow.h @@ -0,0 +1,21 @@ +#import + +static NSString* const kOakCommitWindowServerConnectionName = @"com.macromates.textmate.commit-window-server"; +static NSString* const kOakCommitWindowClientPortName = @"clientPortName"; +static NSString* const kOakCommitWindowArguments = @"arguments"; +static NSString* const kOakCommitWindowEnvironment = @"environment"; +static NSString* const kOakCommitWindowStandardOutput = @"stdout"; +static NSString* const kOakCommitWindowStandardError = @"stderr"; +static NSString* const kOakCommitWindowReturnCode = @"returnCode"; + +@protocol OakCommitWindowClientProtocol +- (void)connectFromServerWithOptions:(NSDictionary*)someOptions; +@end + +@protocol OakCommitWindowServerProtocol +- (void)connectFromClientWithOptions:(NSDictionary*)someOptions; +@end + +PUBLIC @interface OakCommitWindowServer : NSObject ++ (instancetype)sharedInstance; +@end diff --git a/Frameworks/CommitWindow/src/CommitWindow.mm b/Frameworks/CommitWindow/src/CommitWindow.mm new file mode 100644 index 00000000..62188e67 --- /dev/null +++ b/Frameworks/CommitWindow/src/CommitWindow.mm @@ -0,0 +1,192 @@ +#import "CommitWindow.h" +#import + +@interface OakCommitWindow : NSWindowController +@property (nonatomic) NSArray* paths; +@property (nonatomic) NSString* clientPortName; +@property (nonatomic) NSScrollView* scrollView; +@property (nonatomic) NSTableView* tableView; +@property (nonatomic) OakCommitWindow* retainedSelf; +@end + +@implementation OakCommitWindow +- (id)init +{ + if((self = [super init])) + { + _paths = @[ + @{ @"path" : @"/path/to/foo" }, + @{ @"path" : @"/path/to/bar" }, + ]; + + NSTableColumn* tableColumn = [[NSTableColumn alloc] initWithIdentifier:@"path"]; + tableColumn.editable = NO; + tableColumn.dataCell = [[NSTextFieldCell alloc] initTextCell:@""]; + [tableColumn.dataCell setLineBreakMode:NSLineBreakByTruncatingMiddle]; + + NSTableView* tableView = [[NSTableView alloc] initWithFrame:NSZeroRect]; + [tableView addTableColumn:tableColumn]; + tableView.headerView = nil; + tableView.focusRingType = NSFocusRingTypeNone; + tableView.usesAlternatingRowBackgroundColors = YES; + tableView.doubleAction = @selector(didDoubleClickTableView:); + tableView.target = self; + tableView.dataSource = self; + _tableView = tableView; + + _scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect]; + _scrollView.hasVerticalScroller = YES; + _scrollView.hasHorizontalScroller = NO; + _scrollView.autohidesScrollers = YES; + _scrollView.borderType = NSNoBorder; + _scrollView.documentView = _tableView; + + self.window = [[NSWindow alloc] initWithContentRect:NSMakeRect(600, 700, 400, 500) styleMask:(NSTitledWindowMask|NSClosableWindowMask|NSResizableWindowMask|NSMiniaturizableWindowMask|NSTexturedBackgroundWindowMask) backing:NSBackingStoreBuffered defer:NO]; + self.window.delegate = self; + self.window.level = NSFloatingWindowLevel; + self.window.releasedWhenClosed = NO; + self.window.title = @"Commit"; + + NSButton* commitButton = OakCreateButton(@"Commit", NSTexturedRoundedBezelStyle); + NSButton* cancelButton = OakCreateButton(@"Cancel", NSTexturedRoundedBezelStyle); + + commitButton.action = @selector(performCommit:); + cancelButton.action = @selector(cancel:); + + NSDictionary* views = @{ + @"scrollView" : self.scrollView, + @"bottomDivider" : OakCreateHorizontalLine([NSColor grayColor], [NSColor lightGrayColor]), + @"cancel" : cancelButton, + @"commit" : commitButton, + }; + + NSView* contentView = self.window.contentView; + for(NSView* view in [views allValues]) + { + [view setTranslatesAutoresizingMaskIntoConstraints:NO]; + [contentView addSubview:view]; + } + + [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView(==bottomDivider)]|" options:0 metrics:nil views:views]]; + [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[cancel]-[commit]-(8)-|" options:NSLayoutFormatAlignAllBaseline metrics:nil views:views]]; + [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[scrollView(>=50)][bottomDivider]-(5)-[commit]-(6)-|" options:0 metrics:nil views:views]]; + + self.window.defaultButtonCell = commitButton.cell; + } + return self; +} + +- (void)showWindow:(id)sender +{ + [self.window recalculateKeyViewLoop]; + [super showWindow:sender]; + + self.retainedSelf = self; +} + +- (void)windowWillClose:(NSNotification*)aNotification +{ + [self sendCommitMessageToClient:YES]; +} + +- (void)sendCommitMessageToClient:(BOOL)success +{ + if(!self.clientPortName) // Reply already sent + return; + + if(id proxy = [NSConnection rootProxyForConnectionWithRegisteredName:self.clientPortName host:nil]) + { + [proxy setProtocolForProxy:@protocol(OakCommitWindowClientProtocol)]; + + if(success) + { + [proxy connectFromServerWithOptions:@{ + kOakCommitWindowStandardOutput : @"Hello world", + kOakCommitWindowStandardError : @"", + kOakCommitWindowReturnCode : @0, + }]; + } + else + { + [proxy connectFromServerWithOptions:@{ + kOakCommitWindowReturnCode : @1, + }]; + } + + self.clientPortName = nil; + } + + [self performSelector:@selector(setRetainedSelf:) withObject:nil afterDelay:0]; +} + +// ================== +// = Action Methods = +// ================== + +- (void)didDoubleClickTableView:(id)sender +{ + if(_tableView.clickedRow == -1) + return; + + NSDictionary* row = _paths[_tableView.clickedRow]; + NSLog(@"%s show diff for %@", sel_getName(_cmd), row); +} + +- (void)performCommit:(id)sender +{ + [self sendCommitMessageToClient:YES]; + [self close]; +} + +- (void)cancel:(id)sender +{ + [self sendCommitMessageToClient:NO]; + [self close]; +} + +// ========================= +// = NSTableViewDataSource = +// ========================= + +- (NSInteger)numberOfRowsInTableView:(NSTableView*)aTableView +{ + return [_paths count]; +} + +- (id)tableView:(NSTableView*)aTableView objectValueForTableColumn:(NSTableColumn*)aTableColumn row:(NSInteger)rowIndex +{ + NSDictionary* row = _paths[rowIndex]; + return row[aTableColumn.identifier]; +} +@end + +@interface OakCommitWindowServer () +@property (nonatomic) NSConnection* connection; +@end + +@implementation OakCommitWindowServer ++ (instancetype)sharedInstance +{ + static OakCommitWindowServer* sharedInstance = [self new]; + return sharedInstance; +} + +- (id)init +{ + if(self = [super init]) + { + _connection = [NSConnection new]; + [_connection setRootObject:self]; + if([_connection registerName:kOakCommitWindowServerConnectionName] == NO) + NSLog(@"failed to setup connection ‘%@’", kOakCommitWindowServerConnectionName); + } + return self; +} + +- (void)connectFromClientWithOptions:(NSDictionary*)someOptions +{ + OakCommitWindow* commitWindow = [[OakCommitWindow alloc] init]; + commitWindow.clientPortName = someOptions[kOakCommitWindowClientPortName]; + [commitWindow showWindow:self]; +} +@end diff --git a/Frameworks/CommitWindow/target b/Frameworks/CommitWindow/target new file mode 100644 index 00000000..72397aa8 --- /dev/null +++ b/Frameworks/CommitWindow/target @@ -0,0 +1,4 @@ +SOURCES = src/*.mm +EXPORT = src/CommitWindow.h +LINK += OakAppKit OakFoundation +FRAMEWORKS = Cocoa