// // MyDocument.m // R console document // RemoteR // // Created by Simon Urbanek on 2/11/11. // Copyright 2011 Simon Urbanek. All rights reserved. // #import "MyDocument.h" #import "HubSession.h" /*--- console document ---*/ @implementation MyDocument // currentConsole is used by console-independent parts like the editor to identify the console to use for execution static MyDocument *currentConsole; + (MyDocument*) currentConsole { return currentConsole; } - (id)init { self = [super init]; if (self) { // FIXME: this the our current test hack - we need to make this generic at some point - like supporting ssh etc. int p_in[2], p_out[2]; pipe(p_in); pipe(p_out); pid = fork(); if (pid == -1) { // fork error } else if (pid == 0) { // child dup2(p_in[0], STDIN_FILENO); close(p_in[1]); dup2(p_out[1], STDOUT_FILENO); close(p_out[0]); execlp("R", "R", "CMD", "remote", "--no-save", NULL); exit(1); } close(p_out[1]); close(p_in[0]); history = [[History alloc] init]; session = [[HubSession alloc] initWithInput:p_out[0] output:p_in[1]]; // session = [[HubSession alloc] initWithInput:STDIN_FILENO output:STDOUT_FILENO]; [session setConsoleDocument: self]; inputPosition = promptPosition = 0; if (currentConsole) [currentConsole release]; currentConsole = [self retain]; // If an error occurs here, send a [self release] message and return nil. } return self; } // handle closing - kill the associated process (maybe not so it can be re-attached...?) - (void)shouldCloseWindowController:(NSWindowController *)windowController delegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo { if (pid != 0 && pid != -1) kill(pid, SIGTERM); [super shouldCloseWindowController:windowController delegate:delegate shouldCloseSelector:shouldCloseSelector contextInfo:contextInfo]; } - (void) close { if (pid != 0 && pid != -1) kill(pid, SIGTERM); [super close]; } - (NSString *)windowNibName { // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead. return @"MyDocument"; } // --- accessors - (void) setFont: (NSFont*) newFont { NSLog(@"%@> setFont: %@", self, newFont); if (font) [font release]; font = newFont; [font retain]; } - (NSFont*) font { return font; } //---- HUB handling - (void) hubThread: (id) foo { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; hub_session_t *hs = session->session; while (1) { if (hs_wait_for_connections(hs, 5.0)) [self performSelectorOnMainThread:@selector(hubHandleConnections:) withObject:nil waitUntilDone:YES]; } [pool release]; } - (void) hubHandleConnections: (id) foo { hub_session_t *hs = session->session; hs_handle_connections(hs, 0.0); } - (hub_res_t) service: (hub_service_t) service connection: (hub_conn_t*) hc { // HubSession handles the basics such as in/out but any custom services (like Quartz) are handled here return HCERR_OK; } //----- initialization - (void)windowControllerDidLoadNib:(NSWindowController *) aController { [super windowControllerDidLoadNib:aController]; // FIXME: set the font manually for now [self setFont:[NSFont fontWithName:@"Monaco" size:11.0]]; [NSThread detachNewThreadSelector:@selector(hubThread:) toTarget:self withObject:nil]; } - (void) executeInput:(NSString *)txt { if (txt) { // remove trailing whitespaces NSInteger cp = [txt length]; while (cp--) { unichar ch = [txt characterAtIndex:cp]; if (ch != ' ' && ch != '\n' && ch != '\t' || ch != '\r') break; } if (cp + 1 != [txt length]) txt = [txt substringToIndex:cp + 1]; [history commit:txt]; } txt = txt ? [txt stringByAppendingString:@"\n"] : @"\n"; NSInteger inputLength = [[textView string] length] - inputPosition; [textView replaceCharactersInRange:NSMakeRange(inputPosition, inputLength) withString:txt]; [textView setTextColor:[NSColor colorWithCalibratedRed:0.2 green:0.2 blue:1.0 alpha:1.0] range:NSMakeRange(inputPosition, [txt length])]; hub_conn_t *c_in = session->c_in; NSLog(@" inputPosition=%d, c_in=%p, txt='%@'", inputPosition, c_in, txt); if (c_in) { hc_send(c_in, [txt UTF8String], strlen([txt UTF8String])); hc_flush_out(c_in); } promptPosition = inputPosition = [[textView string] length]; [textView scrollRangeToVisible:NSMakeRange(inputPosition, 0)]; } //------ textView control - (BOOL)textView:(NSTextView *)delegateTextView doCommandBySelector:(SEL)commandSelector { if (delegateTextView != textView) return NO; NSLog(@"RController.textView doCommandBySelector: %@\n", NSStringFromSelector(commandSelector)); // ENTER - submit input if (commandSelector == @selector(insertNewline:)) { NSInteger inputLength = [[textView string] length] - inputPosition; NSString *txt = [[textView string] substringWithRange:NSMakeRange(inputPosition, inputLength)]; [self executeInput:txt]; return YES; } // UP - history if (commandSelector == @selector(moveUp:)) { NSInteger textLength = [[textView string] length]; NSRange sr = [textView selectedRange]; if (sr.location >= inputPosition) { NSRange rr = NSMakeRange(inputPosition, textLength - inputPosition); NSString *text = [[textView string] substringWithRange:rr]; if ([history isDirty]) [history updateDirty: text]; NSString *news = [history prev]; if (news) { sr.length = 0; sr.location = inputPosition; [textView setSelectedRange:sr]; [textView replaceCharactersInRange:rr withString:news]; [textView scrollRangeToVisible:NSMakeRange([[textView string] length], 0)]; } return YES; } } // DOWN - history if (commandSelector == @selector(moveDown:)) { unsigned textLength = [[textView string] length]; NSRange sr = [textView selectedRange]; if (sr.location >= inputPosition && ![history isDirty]) { NSRange rr = NSMakeRange(inputPosition, textLength - inputPosition); NSString *news = [history next]; if (!news) news = @""; sr.length = 0; sr.location = inputPosition; [textView setSelectedRange:sr]; [textView replaceCharactersInRange:rr withString:news]; [textView scrollRangeToVisible:NSMakeRange([[textView string] length], 0)]; return YES; } } return NO; } - (BOOL)textView:(NSTextView *)textView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString *)replacementString { if (replacementString && /* on font change we get nil replacementString which is ok to pass through */ affectedCharRange.location < inputPosition) { /* if the insertion is outside editable scope, append at the end */ // [textView setSelectedRange:NSMakeRange([[textView textStorage] length],0)]; // [textView insertText:replacementString]; return NO; } return YES; } - (void) writeConsoleBytes: (const char*) bytes length: (NSInteger) length mode: (int) mode { NSString *txt = [[NSString alloc] initWithBytes:bytes length:length encoding:NSUTF8StringEncoding]; NSInteger len = [[textView string] length], where = promptPosition; if (where < 0) where = len; [textView replaceCharactersInRange:NSMakeRange(where,0) withString:txt]; NSInteger post = [[textView string] length]; if (mode == 3) { /* prompt */ promptPosition = where; inputPosition = where + post - len; [textView setTextColor:[NSColor colorWithCalibratedRed:0.8 green:0.0 blue:1.0 alpha:1.0] range:NSMakeRange(promptPosition, inputPosition - promptPosition)]; } else { if (mode == 2) [textView setTextColor:[NSColor colorWithCalibratedRed:1.0 green:0.0 blue:0.0 alpha:1.0] range:NSMakeRange(where, post - len)]; if (inputPosition >= promptPosition && inputPosition >= 0) inputPosition += post - len; if (promptPosition >= 0) promptPosition += post - len; } [textView scrollRangeToVisible:NSMakeRange([[textView string] length], 0)]; [txt release]; } //---- LOAD/SAVE - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { // Insert code here to write your document to data of the specified type. If the given outError != NULL, ensure that you set *outError when returning nil. // You can also choose to override -fileWrapperOfType:error:, -writeToURL:ofType:error:, or -writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead. // For applications targeted for Panther or earlier systems, you should use the deprecated API -dataRepresentationOfType:. In this case you can also choose to override -fileWrapperRepresentationOfType: or -writeToFile:ofType: instead. if ( outError != NULL ) { *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL]; } return nil; } - (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError { // Insert code here to read your document from the given data of the specified type. If the given outError != NULL, ensure that you set *outError when returning NO. // You can also choose to override -readFromFileWrapper:ofType:error: or -readFromURL:ofType:error: instead. // For applications targeted for Panther or earlier systems, you should use the deprecated API -loadDataRepresentation:ofType. In this case you can also choose to override -readFromFile:ofType: or -loadFileWrapperRepresentation:ofType: instead. if ( outError != NULL ) { *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL]; } return YES; } @end