Merged [15040]: Trying some magic: 5 seconds after the last unreachable host is repor...
[adiumx.git] / Source / GBApplescriptFiltersPlugin.m
blob14cea55c6a9b70527e60e5d45774733e6c1b0752
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "AIContentController.h"
18 #import "AIMenuController.h"
19 #import "AIToolbarController.h"
20 #import "GBApplescriptFiltersPlugin.h"
21 #import <AIUtilities/AIMenuAdditions.h>
22 #import <AIUtilities/AIToolbarUtilities.h>
23 #import <AIUtilities/ESImageAdditions.h>
24 #import <AIUtilities/MVMenuButton.h>
25 #import <AIUtilities/AIExceptionHandlingUtilities.h>
26 #import <Adium/AIContentObject.h>
27 #import <Adium/AIHTMLDecoder.h>
29 #define TITLE_INSERT_SCRIPT             AILocalizedString(@"Insert Script",nil)
30 #define SCRIPT_BUNDLE_EXTENSION @"AdiumScripts"
31 #define SCRIPTS_PATH_NAME               @"Scripts"
32 #define SCRIPT_EXTENSION                @"scpt"
33 #define SCRIPT_IDENTIFIER               @"InsertScript"
35 #define SCRIPT_TIMEOUT                  30
37 @interface GBApplescriptFiltersPlugin (PRIVATE)
39 - (void)_replaceKeyword:(NSString *)keyword
40                          withScript:(NSMutableDictionary *)infoDict
41                            inString:(NSString *)inString
42          inAttributedString:(NSMutableAttributedString *)attributedString
43                            uniqueID:(unsigned long long)uniqueID;
45 - (void)_executeScript:(NSMutableDictionary *)infoDict 
46                  withArguments:(NSArray *)arguments
47                  forAttributedString:(NSMutableAttributedString *)attributedString
48                   keywordRange:(NSRange)keywordRange
49                           uniqueID:(unsigned long long)uniqueID;
51 - (NSArray *)_argumentsFromString:(NSString *)inString forScript:(NSMutableDictionary *)scriptDict;
53 - (void)buildScriptMenu;
54 - (void)_appendScripts:(NSArray *)scripts toMenu:(NSMenu *)menu;
55 - (void)_sortScriptsByTitle:(NSMutableArray *)sortArray;
57 - (void)registerToolbarItem;
59 - (void)scriptDidFinish:(NSNotification *)aNotification;
60 @end
62 int _scriptTitleSort(id scriptA, id scriptB, void *context);
63 int _scriptKeywordLengthSort(id scriptA, id scriptB, void *context);
65 /*!
66  * @class GBApplescriptFiltersPlugin
67  * @brief Filter component to allow .AdiumScripts applescript-based filters for outgoing messages
68  */
69 @implementation GBApplescriptFiltersPlugin
71 /*!
72  * @brief Install
73  */
74 - (void)installPlugin
76         //User scripts
77         [[AIObject sharedAdiumInstance] createResourcePathForName:@"Scripts"];
78         
79         //We have an array of scripts for building the menu, and a dictionary of scripts used for the actual substition
80         scriptArray = nil;
81         flatScriptArray = nil;
82         
83         //Prepare our script menu item (which will have the Scripts menu as its submenu)
84         scriptMenuItem = [[NSMenuItem alloc] initWithTitle:TITLE_INSERT_SCRIPT 
85                                                                                                 target:self
86                                                                                                 action:@selector(dummyTarget:)
87                                                                                  keyEquivalent:@""];
89         //Perform substitutions on outgoing content; we may be slow, so register as a delayed content filter
90         [[adium contentController] registerDelayedContentFilter:self 
91                                                                                                          ofType:AIFilterContent
92                                                                                                   direction:AIFilterOutgoing];
93         
94         //Observe for installation of new scripts
95         [[adium notificationCenter] addObserver:self
96                                                                    selector:@selector(xtrasChanged:)
97                                                                            name:Adium_Xtras_Changed
98                                                                          object:nil];
99         [[NSNotificationCenter defaultCenter] addObserver:self
100                                                                                          selector:@selector(toolbarWillAddItem:)
101                                                                                                  name:NSToolbarWillAddItemNotification
102                                                                                            object:nil]; 
103         
104         //Start building the script menu
105         scriptMenu = nil;
106         [self buildScriptMenu]; //this also sets the submenu for the menu item.
107         
108         [[adium menuController] addMenuItem:scriptMenuItem toLocation:LOC_Edit_Additions];
109         
110         contextualScriptMenuItem = [scriptMenuItem copy];
111         [[adium menuController] addContextualMenuItem:contextualScriptMenuItem toLocation:Context_TextView_Edit];
115  * @brief Deallocate
116  */
117 - (void)dealloc
119         [[NSNotificationCenter defaultCenter] removeObserver:self];
120         [[adium notificationCenter] removeObserver:self];
121         
122         [scriptArray release]; scriptArray = nil;
123     [flatScriptArray release]; flatScriptArray = nil;
124         [scriptMenuItem release]; scriptMenuItem = nil;
125         [contextualScriptMenuItem release]; contextualScriptMenuItem = nil;
126         
127         [super dealloc];
131  * @brief Xtras changes
133  * If the scripts xtras changed, rebuild our menus.
134  */
135 - (void)xtrasChanged:(NSNotification *)notification
137         if ([[notification object] caseInsensitiveCompare:@"AdiumScripts"] == 0) {
138                 [self buildScriptMenu];
139                                 
140                 [self registerToolbarItem];
141                 
142                 //Update our toolbar item's menu
143                 //[self toolbarWillAddItem:nil];
144         }
148 //Script Loading -------------------------------------------------------------------------------------------------------
149 #pragma mark Script Loading
151  * @brief Load our scripts
153  * This will clear out and then load from available scripts (external and internal) into flatScriptArray and scriptArray.
154  */
155 - (void)loadScripts
157         NSEnumerator    *enumerator;
158         NSString                *filePath;
159         NSBundle                *scriptBundle;
161         //
162         [scriptArray release]; scriptArray = [[NSMutableArray alloc] init];
163         [flatScriptArray release]; flatScriptArray = [[NSMutableArray alloc] init];
164         
165         // Load scripts
166         enumerator = [[adium allResourcesForName:@"Scripts" withExtensions:SCRIPT_BUNDLE_EXTENSION] objectEnumerator];
167         while ((filePath = [enumerator nextObject])) {
168                 if ((scriptBundle = [NSBundle bundleWithPath:filePath])) {
169                         
170                         NSString                *scriptsSetName;
171                         NSEnumerator    *scriptEnumerator;
172                         NSDictionary    *scriptDict;
173                         
174                         //Get the name of the set these scripts will go into
175                         scriptsSetName = [scriptBundle objectForInfoDictionaryKey:@"Set"];
176                         
177                         //Now enumerate each script the bundle claims as its own
178                         scriptEnumerator = [[scriptBundle objectForInfoDictionaryKey:@"Scripts"] objectEnumerator];
179                         
180                         while ((scriptDict = [scriptEnumerator nextObject])) {
181                                 NSString                *scriptFileName, *scriptFilePath, *keyword, *title;
182                                 NSArray                 *arguments;
183                                 NSNumber                *prefixOnlyNumber;
184                                 
185                                 if ((scriptFileName = [scriptDict objectForKey:@"File"]) &&
186                                         (scriptFilePath = [scriptBundle pathForResource:scriptFileName
187                                                                                                                          ofType:SCRIPT_EXTENSION])) {
188                                         
189                                         keyword = [scriptDict objectForKey:@"Keyword"];
190                                         title = [scriptDict objectForKey:@"Title"];
191                                         
192                                         if (keyword && [keyword length] && title && [title length]) {
193                                                 NSMutableDictionary     *infoDict;
194                                                 
195                                                 arguments = [[scriptDict objectForKey:@"Arguments"] componentsSeparatedByString:@","];
196                                                 
197                                                 //Assume "Prefix Only" is NO unless told otherwise or the keyword starts with '/'
198                                                 prefixOnlyNumber = [scriptDict objectForKey:@"Prefix Only"];
199                                                 if (!prefixOnlyNumber) {
200                                                         prefixOnlyNumber = [NSNumber numberWithBool:([keyword characterAtIndex:0] == '/')];
201                                                 }
203                                                 infoDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
204                                                         scriptFilePath, @"Path", keyword, @"Keyword", title, @"Title", 
205                                                         prefixOnlyNumber, @"PrefixOnly", nil];
206                                                 
207                                                 //The bundle may not be part of (or for defining) a set of scripts
208                                                 if (scriptsSetName) {
209                                                         [infoDict setObject:scriptsSetName forKey:@"Set"];
210                                                 }
211                                                 //Arguments may be nil
212                                                 if (arguments) {
213                                                         [infoDict setObject:arguments forKey:@"Arguments"];
214                                                 }
215                                                 
216                                                 //Place the entry in our script arrays
217                                                 [scriptArray addObject:infoDict];
218                                                 [flatScriptArray addObject:infoDict];
219                                                 
220                                                 //Scripts must always be updated via polling
221                                                 [[adium contentController] registerFilterStringWhichRequiresPolling:keyword];
222                                         }
223                                 }
224                         }
225                 }               
226         }
230 //Script Menu ----------------------------------------------------------------------------------------------------------
231 #pragma mark Script Menu
233  * @brief Build the script menu
235  * Loads the scrpts as necessary, sorts them, then builds menus for the menu bar, the contextual menu,
236  * and the toolbar item.
237  */
238 - (void)buildScriptMenu
240         [self loadScripts];
241         
242         //Sort the scripts
243         [scriptArray sortUsingFunction:_scriptTitleSort context:nil];
244         [flatScriptArray sortUsingFunction:_scriptKeywordLengthSort context:nil];
245         
246         //Build the menu
247         [scriptMenu release]; scriptMenu = [[NSMenu alloc] initWithTitle:TITLE_INSERT_SCRIPT];
248         [self _appendScripts:scriptArray toMenu:scriptMenu];
249         [scriptMenuItem setSubmenu:scriptMenu];
250         [contextualScriptMenuItem setSubmenu:[[scriptMenu copy] autorelease]];
251                 
252         [self registerToolbarItem];
256  * @brief Sort first by set, then by title within sets
257  */
258 int _scriptTitleSort(id scriptA, id scriptB, void *context) {
259         NSComparisonResult result;
260         
261         NSString        *setA = [scriptA objectForKey:@"Set"];
262         NSString        *setB = [scriptB objectForKey:@"Set"];
263         
264         if (setA && setB) {
265                 
266                 //If both are within sets, sort by set; if they are within the same set, sort by title
267                 if ((result = [setA caseInsensitiveCompare:setB]) == NSOrderedSame) {
268                         result = [(NSString *)[scriptA objectForKey:@"Title"] caseInsensitiveCompare:[scriptB objectForKey:@"Title"]];
269                 }
270         } else {
271                 //Sort by title if neither is in a set; otherwise sort the one in a set to the top
272                 
273                 if (!setA && !setB) {
274                         result = [(NSString *)[scriptA objectForKey:@"Title"] caseInsensitiveCompare:[scriptB objectForKey:@"Title"]];
275                 
276                 } else if (!setA) {
277                         result = NSOrderedDescending;
278                 } else {
279                         result = NSOrderedAscending;
280                 }
281         }
282         
283         return(result);
287  * @brief Sort by descending length so the longest keywords are at the beginning of the array
288  */
289 int _scriptKeywordLengthSort(id scriptA, id scriptB, void *context)
291         NSComparisonResult result;
292         
293         unsigned int lengthA = [(NSString *)[scriptA objectForKey:@"Keyword"] length];
294         unsigned int lengthB = [(NSString *)[scriptB objectForKey:@"Keyword"] length];
295         if (lengthA > lengthB) {
296                 result = NSOrderedAscending;
297         } else if (lengthA < lengthB) {
298                 result = NSOrderedDescending;
299         } else {
300                 result = NSOrderedSame;
301         }
302         
303         return result;
307  * @brief Append an array of scripts to a menu
309  * @param scripts The scripts, each of which is represented by an NSDictionary instance
310  * @param menu The menu to which to add the scripts
311  */
312 - (void)_appendScripts:(NSArray *)scripts toMenu:(NSMenu *)menu
314         NSEnumerator    *enumerator;
315         NSDictionary    *appendDict;
316         NSString                *lastSet = nil;
317         NSString                *set;
318         int                             indentationLevel;
319         
320         enumerator = [scripts objectEnumerator];
321         while ((appendDict = [enumerator nextObject])) {
322                 NSString        *title;
323                 NSMenuItem      *item;
324                 
325                 if ((set = [appendDict objectForKey:@"Set"])) {
326                         indentationLevel = 1;
327                         
328                         if (![set isEqualToString:lastSet]) {
329                                 //We have a new set of scripts; create a section header for them
330                                 item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:set
331                                                                                                                                                          target:nil
332                                                                                                                                                          action:nil
333                                                                                                                                           keyEquivalent:@""] autorelease];
334                                 if ([item respondsToSelector:@selector(setIndentationLevel:)]) [item setIndentationLevel:0];
335                                 [menu addItem:item];
336                                 
337                                 [lastSet release]; lastSet = [set retain];
338                         }
339                 } else {
340                         //Scripts not in sets need not be indented
341                         indentationLevel = 0;
342                         [lastSet release]; lastSet = nil;
343                 }
344         
345                 if ([appendDict objectForKey:@"Title"]) {
346                         title = [NSString stringWithFormat:@"%@ (%@)", [appendDict objectForKey:@"Title"], [appendDict objectForKey:@"Keyword"]];
347                 } else {
348                         title = [appendDict objectForKey:@"Keyword"];
349                 }
350                 
351                 item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
352                                                                                                                                          target:self
353                                                                                                                                          action:@selector(selectScript:)
354                                                                                                                           keyEquivalent:@""] autorelease];
355                 
356                 [item setRepresentedObject:appendDict];
357                 if ([item respondsToSelector:@selector(setIndentationLevel:)]) [item setIndentationLevel:indentationLevel];
358                 [menu addItem:item];
359         }
363  * @brief Insert a script's keyword into the text entry area
365  * This will be called by an NSMenuItem when it is clicked.
366  */
367 - (IBAction)selectScript:(id)sender
369         NSResponder     *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
370         
371         //Append our string into the responder if possible
372         if (responder && [responder isKindOfClass:[NSTextView class]]) {
373                 NSArray         *arguments = [[sender representedObject] objectForKey:@"Arguments"];
374                 NSString        *replacementText = [[sender representedObject] objectForKey:@"Keyword"];
375                 
376                 [(NSTextView *)responder insertText:replacementText];
377                 
378                 //Append arg list to replacement string, to show the user what they can pass
379                 if (arguments) {
380                         NSEnumerator            *argumentEnumerator = [arguments objectEnumerator];
381                         NSDictionary            *originalTypingAttributes = [(NSTextView *)responder typingAttributes];
382                         NSMutableDictionary *italicizedTypingAttributes = [originalTypingAttributes mutableCopy];
383                         NSString                        *anArgument;
384                         BOOL                            insertedFirst = NO;
385                         
386                         [italicizedTypingAttributes setObject:[[NSFontManager sharedFontManager] convertFont:[originalTypingAttributes objectForKey:NSFontAttributeName]
387                                                                                                                                                                          toHaveTrait:NSItalicFontMask]
388                                                                                    forKey:NSFontAttributeName];
389                         
390                         [(NSTextView *)responder insertText:@"{"];
391                         
392                         //Will that be a five minute argument or the full half hour?
393                         while ((anArgument = [argumentEnumerator nextObject])) {
394                                 //Insert a comma after each argument past the first
395                                 if (insertedFirst) {
396                                         [(NSTextView *)responder insertText:@","];                                      
397                                 } else {
398                                         insertedFirst = YES;
399                                 }
400                                 
401                                 //Turn on the italics version, insert the argument, then go back to normal for either the comma or the ending
402                                 [(NSTextView *)responder setTypingAttributes:italicizedTypingAttributes];
403                                 [(NSTextView *)responder insertText:anArgument];
404                                 [(NSTextView *)responder setTypingAttributes:originalTypingAttributes];
405                         }
407                         [(NSTextView *)responder insertText:@"}"];
408                         
409                         [italicizedTypingAttributes release];
410                 }
411         }
415  * @brief Fake target to allow validateMenuItem: to be called
416  */
417 -(IBAction)dummyTarget:(id)sender{
421  * @brief Validate menu item
422  * Disable the insertion if a text field is not active
423  */
424 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
426         if ((menuItem == scriptMenuItem) || (menuItem == contextualScriptMenuItem)) {
427                 return(YES); //Always keep the submenu enabled so users can see the available scripts
428         } else {
429                 NSResponder     *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
430                 if (responder && [responder isKindOfClass:[NSText class]]) {
431                         return [(NSText *)responder isEditable];
432                 } else {
433                         return NO;
434                 }
435         }
438 //Message Filtering ----------------------------------------------------------------------------------------------------
439 #pragma mark Message Filtering
441  * @brief Delayed filter messages for keywords to replace
443  * Will eventually replace any script keywords with the result of running the script (with arguments as appropriate).
444  * @result YES if we began a delayed filtration; NO if we did not
445  */
446 - (BOOL)delayedFilterAttributedString:(NSAttributedString *)inAttributedString context:(id)context uniqueID:(unsigned long long)uniqueID
448         BOOL            beganProcessing = NO; 
449         NSString        *stringMessage;
451         if ((stringMessage = [inAttributedString string])) {
452                 NSEnumerator                            *enumerator;
453                 NSMutableDictionary                     *infoDict;
454                 
455                 //Replace all keywords
456                 enumerator = [flatScriptArray objectEnumerator];
457                 while ((infoDict = [enumerator nextObject]) && !beganProcessing) {
458                         NSString        *keyword = [infoDict objectForKey:@"Keyword"];
459                         BOOL            prefixOnly = [[infoDict objectForKey:@"PrefixOnly"] boolValue];
461                         if ((prefixOnly && ([stringMessage rangeOfString:keyword options:(NSCaseInsensitiveSearch | NSAnchoredSearch)].location == 0)) ||
462                            (!prefixOnly && [stringMessage rangeOfString:keyword options:NSCaseInsensitiveSearch].location != NSNotFound)) {
463                                 NSNumber        *shouldSendNumber;
465                                 [self _replaceKeyword:keyword
466                                                    withScript:infoDict
467                                                          inString:stringMessage
468                                    inAttributedString:[[inAttributedString mutableCopy] autorelease]
469                                                          uniqueID:uniqueID];
471                                 shouldSendNumber = [infoDict objectForKey:@"ShouldSend"];
472                                 if ((shouldSendNumber) &&
473                                         (![shouldSendNumber boolValue]) &&
474                                         ([context isKindOfClass:[AIContentObject class]])) {
475                                         [(AIContentObject *)context setSendContent:NO];
476                                 }
477                                 
478                                 beganProcessing = YES;
479                         }
480                 }
481         }
482         
483     return beganProcessing;
487  * @brief Filter priority
489  * Filter earlier than the default
490  */
491 - (float)filterPriority
493         return HIGH_FILTER_PRIORITY;
497  * @brief Perform a thorough variable replacing scan
498  */
499 - (void)_replaceKeyword:(NSString *)keyword
500                          withScript:(NSMutableDictionary *)infoDict
501                            inString:(NSString *)inString
502          inAttributedString:(NSMutableAttributedString *)attributedString
503                            uniqueID:(unsigned long long)uniqueID
505         NSScanner       *scanner;
506         BOOL            foundKeyword = NO;
507         BOOL            beganExecutingScript = NO;
509         //Scan for the keyword
510         scanner = [NSScanner scannerWithString:inString];
511         while (![scanner isAtEnd] && !foundKeyword) {
512                 [scanner scanUpToString:keyword intoString:nil];
513                 
514                 if (([scanner scanString:keyword intoString:nil]) &&
515                         ([attributedString attribute:NSLinkAttributeName
516                                                                  atIndex:([scanner scanLocation]-1) /* The scanner ends up one past the keyword */
517                                                   effectiveRange:nil] == nil)) {
518                         //Scan the keyword and ensure it was not found within a link
519                         int             keywordStart, keywordEnd;
520                         NSArray         *argArray = nil;
521                         NSString        *argString;
522                         
523                         //Scan arguments
524                         keywordStart = [scanner scanLocation] - [keyword length];
525                         if ([scanner scanString:@"{" intoString:nil]) {
526                                 if ([scanner scanUpToString:@"}" intoString:&argString]) {
527                                         argArray = [self _argumentsFromString:argString forScript:infoDict];
528                                         [scanner scanString:@"}" intoString:nil];
529                                 }
530                         }
531                         keywordEnd = [scanner scanLocation];            
532                         
533                         if (keywordStart != 0 && [inString characterAtIndex:keywordStart - 1] == '\\') {
534                                 //Ignore the script (It was escaped) and delete the escape character
535                                 [attributedString replaceCharactersInRange:NSMakeRange(keywordStart - 1, 1) withString:@""];
536                                 foundKeyword = YES;
538                         } else {
539                                 //Run the script.
540                                 NSRange keywordRange = NSMakeRange(keywordStart, keywordEnd - keywordStart);
541                                 
542                                 [self _executeScript:infoDict 
543                                            withArguments:argArray
544                                  forAttributedString:attributedString
545                                                 keywordRange:keywordRange
546                                                         uniqueID:uniqueID];
548                                 beganExecutingScript = YES;
549                         }
550                 }
551         }
555  * @brief Execute the script as a separate task
557  * When the task is complete, we will be notified, at which point we perform the replacement for the script result
558  * and pass the modified attributed string back to the content controller for use.
559  */
560 - (void)_executeScript:(NSMutableDictionary *)infoDict 
561                            withArguments:(NSArray *)arguments
562                  forAttributedString:(NSMutableAttributedString *)attributedString
563                                 keywordRange:(NSRange)keywordRange
564                                         uniqueID:(unsigned long long)uniqueID
566         NSAutoreleasePool       *autoreleasePool = [[NSAutoreleasePool alloc] init];
567         NSTask                          *scriptTask;
568         NSMutableArray          *applescriptRunnerArguments = [NSMutableArray arrayWithObject:[infoDict objectForKey:@"Path"]];
569         NSPipe                          *outputPipe;
570         
571         static NSString *applescriptRunnerPath = nil;
572         if (!applescriptRunnerPath) {
573                 //Find and cache the path to the ApplescriptRunner application
574                 applescriptRunnerPath = [[[NSBundle mainBundle] pathForResource:@"AdiumApplescriptRunner"
575                                                                                                                                  ofType:nil
576                                                                                                                         inDirectory:nil] retain];
577         }
579         //We run the substitute function
580         [applescriptRunnerArguments addObject:@"substitute"];
582         if (arguments && [arguments count]) {
583                 [applescriptRunnerArguments addObjectsFromArray:arguments];
584         }
586         scriptTask = [[NSTask alloc] init];
588         //Set up a time out after which the scriptTask will terminate itself
589         [scriptTask performSelector:@selector(terminate)
590                                          withObject:nil
591                                          afterDelay:SCRIPT_TIMEOUT];
593         [scriptTask setLaunchPath:applescriptRunnerPath];
594         [scriptTask setArguments:applescriptRunnerArguments];
596         outputPipe = [[NSPipe alloc] init];
597                 
598         if (outputPipe) {
599                 [scriptTask setStandardOutput:outputPipe];
600                 [outputPipe release];
601         } else {
602                 NSLog(@"could not create pipe: %s", strerror(errno));
603         }
605         [scriptTask setEnvironment:[NSDictionary dictionaryWithObjectsAndKeys:
606                 attributedString, @"Mutable Attributed String",
607                 NSStringFromRange(keywordRange), @"Range",
608                 [NSNumber numberWithUnsignedLongLong:uniqueID], @"uniqueID",
609                 nil]];
611         [[NSNotificationCenter defaultCenter] addObserver:self
612                                                                                          selector:@selector(scriptDidFinish:)
613                                                                                                  name:NSTaskDidTerminateNotification
614                                                                                            object:scriptTask];
615         AI_DURING
616                 [scriptTask launch];
617         AI_HANDLER
618                 //If the task fails to launch, send the termination notification
619                 NSNotification  *notification = [NSNotification notificationWithName:NSTaskDidTerminateNotification
620                                                                                                                                           object:scriptTask];
621                 NSLog(@"Couldn't launch (%@)",localException);
622                 [self scriptDidFinish:notification];
623         AI_ENDHANDLER
624                 
625         [autoreleasePool release];
629  * @brief A script finished executing
631  * @param aNotification The notification, whose object is the NSTask which terminated
632  */
633 - (void)scriptDidFinish:(NSNotification *)aNotification
635         NSAutoreleasePool                       *autoreleasePool = [[NSAutoreleasePool alloc] init];
636         NSTask                                          *scriptTask = [aNotification object];
637         NSDictionary                            *environment = [scriptTask environment];
638         id                                                      standardOutput = [scriptTask standardOutput];
639         NSMutableAttributedString       *attributedString = [environment objectForKey:@"Mutable Attributed String"];
640         NSRange                                         keywordRange = NSRangeFromString([environment objectForKey:@"Range"]);
641         NSNumber                                        *uniqueID = [environment objectForKey:@"uniqueID"];
642         NSFileHandle                            *output = nil;
643         NSString                                        *scriptResult = nil;
644                         
645         if ([standardOutput isKindOfClass:[NSPipe class]] &&
646                 (output = [(NSPipe *)standardOutput fileHandleForReading])) {
647                 AI_DURING
648                         scriptResult = [[NSString alloc] initWithData:[output readDataToEndOfFile]
649                                                                                                  encoding:NSUTF8StringEncoding];
650                 AI_HANDLER
651                         scriptResult = nil;
652                 AI_ENDHANDLER
654                 //The NSFileHandle will be closed automatically when the NSPipe is deallocated... but let's do it immediately
655                 [output closeFile];
656         }
658         //If the script fails, eat the keyword
659         if (!scriptResult) scriptResult = [@"" retain];
661         //Replace the substring with script result
662         if (NSMaxRange(keywordRange) <= [attributedString length]) {
663                 if (([scriptResult hasPrefix:@"<HTML>"])) {
664                         //Obtain the attributed string version of the HTML, passing our current attributes as the default ones
665                         NSAttributedString *attributedScriptResult = [AIHTMLDecoder decodeHTML:scriptResult
666                                                                                                                          withDefaultAttributes:[attributedString attributesAtIndex:keywordRange.location
667                                                                                                                                                                                                                 effectiveRange:nil]];
668                         [attributedString replaceCharactersInRange:keywordRange
669                                                                   withAttributedString:attributedScriptResult];
670                         
671                 } else {
672                         [attributedString replaceCharactersInRange:keywordRange
673                                                                                         withString:scriptResult];
674                 }
675         }
677         //Remove the delayed perform request for termination (which would have been used if the script timed out)
678         [NSObject cancelPreviousPerformRequestsWithTarget:scriptTask
679                                                                                          selector:@selector(terminate)
680                                                                                            object:nil];
682         /* Remove the observer. NSTask objects appear to be reused internally once released, so this will be called multiple
683          * times for future NSTask objects, since there is already an observer for the object, according to -[NSTask isEqual:]
684          * if we don't.
685          */
686         [[NSNotificationCenter defaultCenter] removeObserver:self
687                                                                                                         name:NSTaskDidTerminateNotification
688                                                                                                   object:scriptTask];
690         //Cleanup
691         [scriptResult release];
692         [scriptTask release];
693         [autoreleasePool release];
695         //Inform the content controller that we're done
696         [[adium contentController] delayedFilterDidFinish:attributedString uniqueID:[uniqueID unsignedLongLongValue]];
700  * @brief Determine the arguments for a script execution
702  * @param inString The string of potential arguments
703  * @param scriptDict The script being executed
705  * @result An NSArray of NSString instances
706  */
707 - (NSArray *)_argumentsFromString:(NSString *)inString forScript:(NSMutableDictionary *)scriptDict
709         NSArray                 *scriptArguments = [scriptDict objectForKey:@"Arguments"];
710         NSMutableArray  *argArray = [NSMutableArray array];
711         NSArray                 *inStringComponents = [inString componentsSeparatedByString:@","];
712         
713         unsigned                i = 0;
714         unsigned                count = (scriptArguments ? [scriptArguments count] : 0);
715         unsigned                inStringComponentsCount = [inStringComponents count];
716         
717         //Add each argument of inString to argArray so long as the number of arguments is less
718         //than the number of expected arguments for the script and the number of supplied arguments
719         while ((i < count) && (i < inStringComponentsCount)) {
720                 [argArray addObject:[inStringComponents objectAtIndex:i]];
721                 i++;
722         }
723         
724         //If more components were passed than were actually requested, the last argument gets the
725         //remainder
726         if (i < inStringComponentsCount) {
727                 NSRange remainingRange;
728                 
729                 //i was incremented to end the while loop if i > 0, so subtract 1 to reexamine the last object
730                 remainingRange.location = ((i > 0) ? i-1 : 0);
731                 remainingRange.length = (inStringComponentsCount - remainingRange.location);
733                 if (remainingRange.location >= 0) {
734                         NSString        *lastArgument;
736                         //Remove that last, incomplete argument if it was added
737                         if ([argArray count]) [argArray removeLastObject];
739                         //Create the last argument by joining all remaining comma-separated arguments with a comma
740                         lastArgument = [[inStringComponents subarrayWithRange:remainingRange] componentsJoinedByString:@","];
742                         [argArray addObject:lastArgument];
743                 }
744         }
745         
746         return(argArray);
749 #pragma mark Toolbar item
751  * @brief Register our insert script toolbar item
752  */
753 - (void)registerToolbarItem
755         MVMenuButton *button;
756         
757         //Unregister the existing toolbar item first
758         if (toolbarItem) {
759                 [[adium toolbarController] unregisterToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
760                 [toolbarItem release]; toolbarItem = nil;
761         }
762         
763         //Register our toolbar item
764         button = [[[MVMenuButton alloc] initWithFrame:NSMakeRect(0,0,32,32)] autorelease];
765         [button setImage:[NSImage imageNamed:@"scriptToolbar" forClass:[self class]]];
766         toolbarItem = [[AIToolbarUtilities toolbarItemWithIdentifier:SCRIPT_IDENTIFIER
767                                                                                                                    label:AILocalizedString(@"Scripts",nil)
768                                                                                                         paletteLabel:TITLE_INSERT_SCRIPT
769                                                                                                                  toolTip:AILocalizedString(@"Insert a script",nil)
770                                                                                                                   target:self
771                                                                                                  settingSelector:@selector(setView:)
772                                                                                                          itemContent:button
773                                                                                                                   action:@selector(selectScript:)
774                                                                                                                         menu:nil] retain];
775         [toolbarItem setMinSize:NSMakeSize(32,32)];
776         [toolbarItem setMaxSize:NSMakeSize(32,32)];
777         [button setToolbarItem:toolbarItem];
778     [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
782  * @brief After the toolbar has added the item we can set up the submenus
783  */
784 - (void)toolbarWillAddItem:(NSNotification *)notification
786         NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
787         
788         if (!notification || ([[item itemIdentifier] isEqualToString:SCRIPT_IDENTIFIER])) {
789                 NSMenu          *menu = [[[scriptMenuItem submenu] copy] autorelease];
790                 
791                 //Add menu to view
792                 [[item view] setMenu:menu];
793                 
794                 //Add menu to toolbar item (for text mode)
795                 NSMenuItem      *mItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] init] autorelease];
796                 [mItem setSubmenu:menu];
797                 [mItem setTitle:[menu title]];
798                 [item setMenuFormRepresentation:mItem];
799         }
802 @end