Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/mac-app/AppController.m
8814 views
1
//
2
// AppController.m
3
// SageMenu
4
//
5
// Created by Ivan Andrus on 19/6/10.
6
// Copyright 2010 __MyCompanyName__. All rights reserved.
7
//
8
9
#import "AppController.h"
10
#import "MyDocument.h"
11
#import "InputPanelController.h"
12
#import <WebKit/WebFrame.h>
13
14
15
@implementation AppController
16
17
// With help from
18
// http://www.sonsothunder.com/devres/revolution/tutorials/StatusMenu.html
19
20
- (void) awakeFromNib{
21
22
// Used to detect where our files are
23
NSBundle *bundle = [NSBundle mainBundle];
24
25
// Allocate and load the images into the application which will be used for our NSStatusItem
26
statusImageBlue = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"sage-small-blue" ofType:@"png"]];
27
statusImageRed = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"sage-small-red" ofType:@"png"]];
28
statusImageGrey = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"sage-small-grey" ofType:@"png"]];
29
statusImageGreen = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"sage-small-green" ofType:@"png"]];
30
31
// Access to the user's defaults
32
defaults = [NSUserDefaults standardUserDefaults];
33
34
// Find sageBinary etc.
35
[self setupPaths];
36
37
// Initialize the StatusItem if desired.
38
// If we are on Tiger, then showing in the dock doesn't work
39
// properly, hence pretend they didn't want it.
40
myIsInDock = [defaults boolForKey:@"myShowInDock"] && ![self isTigerOrLess];
41
haveStatusItem = !myIsInDock || [defaults boolForKey:@"alsoShowMenuExtra"];
42
useSystemBrowser = !myIsInDock || [defaults boolForKey:@"useSystemBrowser"];
43
if ( haveStatusItem ) {
44
// Create the NSStatusBar and set its length
45
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
46
47
// Set the image in NSStatusItem
48
[statusItem setImage:statusImageGrey];
49
50
// Tell NSStatusItem what menu to load
51
[statusItem setMenu:statusMenu];
52
// Set the tooptip for our item
53
[statusItem setToolTip:@"Control Sage Notebook Server"];
54
// Enable highlighting when menu is opened
55
[statusItem setHighlightMode:YES];
56
} else {
57
[statusItem setEnabled:NO];
58
}
59
60
// indicate that we haven't started the server yet
61
port = 0;
62
neverOpenedFileBrowser = YES;
63
URLQueue = [[NSMutableArray arrayWithCapacity:3] retain];
64
65
// Start the sage server, or check if it's running
66
if ( [defaults boolForKey:@"startServerOnLaunch"] ) {
67
[self startServer:self];
68
} else {
69
[self serverIsRunning:NO];
70
}
71
72
// Set up notifications when an NSTask finishes.
73
// For us this will be for checking if the server is running
74
[[NSNotificationCenter defaultCenter] addObserver:self
75
selector:@selector(taskTerminated:)
76
name:NSTaskDidTerminateNotification
77
object:nil];
78
}
79
80
- (void) dealloc {
81
// Release the images we loaded into memory
82
[statusImageBlue release];
83
[statusImageRed release];
84
[statusImageGrey release];
85
[statusImageGreen release];
86
[sageBinary release];
87
[logPath release];
88
[theTask release];
89
[taskPipe release];
90
[URLQueue release];
91
[super dealloc];
92
}
93
94
95
-(IBAction)startServer:(id)sender{
96
// TODO: Check to see if it's running before attempting to start
97
NSLog(@"Starting server");
98
if (haveStatusItem) [statusItem setImage:statusImageGreen];
99
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"start-sage" ofType:@"sh"];
100
101
// Add SAGE_BROWSER to environment to point back to this application
102
if ( !useSystemBrowser ) {
103
NSString *browserPath = [[NSBundle mainBundle] pathForResource:@"open-location" ofType:@"sh"];
104
setenv("SAGE_BROWSER", [browserPath UTF8String], 1); // this overwrites, should it?
105
}
106
107
// Create a task to start the server
108
[NSTask launchedTaskWithLaunchPath:scriptPath
109
arguments:[NSArray arrayWithObjects:sageBinary, logPath, nil]];
110
// We now forget about the task. I hope that's okay...
111
112
// Open loading page since it can take a while to start
113
[self browseRemoteURL:[[NSBundle mainBundle] pathForResource:@"loading-page" ofType:@"html"]];
114
115
// Get info about the server if we're not going to get it via opening a page
116
if ( useSystemBrowser ) {
117
[self serverIsRunning:YES];
118
}
119
}
120
121
-(BOOL)serverIsRunning:(BOOL)wait{
122
123
// Start busy polling until the server starts
124
if ( theTask == nil && taskPipe == nil ) {
125
theTask = [[NSTask alloc] init];
126
taskPipe = [[NSPipe alloc] init];
127
[theTask setStandardOutput:taskPipe];
128
[theTask setLaunchPath:[[NSBundle mainBundle] pathForResource:@"sage-is-running-on-port" ofType:@"sh"]];
129
if (wait) [theTask setArguments:[NSArray arrayWithObject:@"--wait"]];
130
[theTask launch];
131
}
132
return NO;
133
}
134
135
-(void)serverStartedWithPort:(int)p{
136
if (haveStatusItem) [statusItem setImage:statusImageBlue];
137
port = p;
138
if ( [URLQueue count] > 0 ) {
139
NSEnumerator *e = [URLQueue objectEnumerator];
140
id url;
141
while (url = [e nextObject]) {
142
[self browseLocalSageURL:url];
143
}
144
[URLQueue removeAllObjects];
145
}
146
}
147
148
- (void)taskTerminated:(NSNotification *)aNotification {
149
150
NSTask *theObject = [aNotification object];
151
if (theObject == theTask) {
152
const int status = [theObject terminationStatus];
153
if (status == 0) {
154
// Parse the output
155
NSData *data = [[taskPipe fileHandleForReading] readDataToEndOfFile];
156
NSString* s = [[NSString alloc] initWithBytes:[data bytes]
157
length:[data length]
158
encoding:NSUTF8StringEncoding];
159
const int p = [s intValue];
160
[s release];
161
[self serverStartedWithPort:p];
162
} else {
163
// We failed, so tell the user
164
if (haveStatusItem) [statusItem setImage:statusImageGrey];
165
port = 0;
166
}
167
// Reset for next time.
168
[theTask release];
169
theTask = nil;
170
[taskPipe release];
171
taskPipe = nil;
172
} else {
173
// NSLog(@"Got called for a different task.");
174
}
175
}
176
177
-(IBAction)stopServer:(id)sender{
178
if (haveStatusItem) [statusItem setImage:statusImageRed];
179
180
// Get the pid of the Sage server
181
NSString *pidFile = [@"~/.sage/sage_notebook.sagenb/twistd.pid" stringByStandardizingPath];
182
NSString *pid = [NSString stringWithContentsOfFile:pidFile
183
encoding:NSUTF8StringEncoding
184
error:NULL];
185
186
if (pid == nil) {
187
// Get the pid of the Sage server
188
pidFile = [@"~/.sage/sage_notebook.sagenb/sagenb.pid" stringByStandardizingPath];
189
pid = [NSString stringWithContentsOfFile:pidFile
190
encoding:NSUTF8StringEncoding
191
error:NULL];
192
}
193
194
NSLog(@"Stopping server with pid: %@", pid );
195
if (pid != nil) {
196
kill([pid intValue], SIGTERM);
197
}
198
199
if (haveStatusItem) [statusItem setImage:statusImageGrey];
200
port = 0;
201
}
202
203
// To create an alternate menu, in IB create another menu item, give it a key equivalent of opt/alt and check the alternate box (left most tab of inspector)
204
-(IBAction)stopServerAndQuit:(id)sender{
205
206
[self stopServer:self];
207
208
// Tell the application to quit
209
[NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0];
210
}
211
212
-(IBAction)viewSageLog:(id)sender{
213
if (logPath != nil) {
214
// open files with the default viewer (I think the default is Console.app)
215
// http://lethain.com/entry/2008/apr/05/opening-files-with-associated-app-in-cocoa/
216
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
217
[workspace openFile:logPath];
218
}
219
}
220
221
-(void)setupPaths{
222
223
// Find the log path
224
NSFileManager *fileMgr = [NSFileManager defaultManager];
225
NSArray *directories = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
226
NSString *tmpLogPath;
227
if ([directories count] > 0) {
228
tmpLogPath = [directories objectAtIndex:0];
229
tmpLogPath = [tmpLogPath stringByAppendingPathComponent:@"Logs"];
230
231
if ( [fileMgr fileExistsAtPath:tmpLogPath] || [fileMgr createDirectoryAtPath:tmpLogPath attributes:nil] ) {
232
logPath = [tmpLogPath stringByAppendingPathComponent:@"sage.log"];
233
/* If we want to send our log there...
234
int fd = open([logPath fileSystemRepresentation], (O_RDWR|O_CREAT|O_TRUNC), (S_IRWXU|S_IRWXG|S_IRWXO));
235
if (fd != -1) {
236
result = asl_add_log_file(NULL, fd);
237
}
238
*/
239
} else {
240
logPath = [[NSBundle mainBundle] pathForResource:@"sage" ofType:@"log"];
241
NSLog(@"Couldn't create the directory (%@) for log file. Going to log to %@.",tmpLogPath,logPath);
242
}
243
} else {
244
logPath = [[NSBundle mainBundle] pathForResource:@"sage" ofType:@"log"];
245
NSLog(@"Something is fishy: couldn't find a path for log files. Going to log to %@.", logPath);
246
}
247
[logPath retain];
248
249
// ### Find the sage binary ###
250
251
// If they have a plist entry telling where it is try that.
252
sageBinary = [defaults objectForKey:@"SageBinary"];
253
// If that isn't wanted or isn't executable, try a sage built in to the application
254
BOOL isDir = YES;
255
// If the file is a directory, see if it's SAGE_ROOT
256
if ( [fileMgr fileExistsAtPath:sageBinary isDirectory:&isDir] && isDir ) {
257
[defaults setObject:[sageBinary stringByAppendingPathComponent:@"sage"]
258
forKey:@"SageBinary"];
259
sageBinary = [defaults objectForKey:@"SageBinary"];
260
}
261
// Put isDir last since technically it's value is undefined if the file doesn't exist
262
if ( ![defaults boolForKey:@"useAltSageBinary"] || ![fileMgr isExecutableFileAtPath:sageBinary] ) {
263
NSString * path = [[NSBundle mainBundle] pathForResource:@"sage" ofType:nil inDirectory:@"sage"];
264
sageBinary = path ? [[NSString alloc] initWithString:path] : nil;
265
[defaults setBool:NO forKey:@"useAltSageBinary"];
266
}
267
268
// If that doesn't work then have them locate a binary for us
269
if ( !sageBinary || ![fileMgr isExecutableFileAtPath:sageBinary] ) {
270
271
// Create a File Open Dialog class
272
NSOpenPanel *openDlg = [NSOpenPanel openPanel];
273
274
// Enable the selection of files and directories
275
[openDlg setTitle:@"Please choose a Sage executable"];
276
[openDlg setMessage:@"This application did not come with a Sage distribution, and there is no valid alternative specified.\n\
277
Please choose a Sage executable to use from now on. If you do not, sage is assumed to be in PATH.\n\
278
You can change it later in Preferences."];
279
[openDlg setCanChooseFiles:YES];
280
[openDlg setCanChooseDirectories:YES];
281
282
// Display the dialog. If the OK button was pressed,
283
// process the files.
284
while ( [openDlg runModalForDirectory:nil file:nil] == NSOKButton ) {
285
sageBinary = [[openDlg filenames] objectAtIndex:0];
286
// if they give a folder, look for sage inside
287
if ( [fileMgr fileExistsAtPath:sageBinary isDirectory:&isDir] && isDir ) {
288
sageBinary = [sageBinary stringByAppendingPathComponent:@"sage"];
289
}
290
// Sanity check for the validity of the Sage Binary
291
if ( [fileMgr isExecutableFileAtPath:sageBinary] ) {
292
// Save for future sessions
293
[defaults setBool:YES forKey:@"useAltSageBinary"];
294
[defaults setObject:sageBinary forKey:@"SageBinary"];
295
[sageBinary retain];
296
return;
297
}
298
[openDlg setMessage:@"That does not appear to be a valid sage executable.\nPlease choose another, or cancel to assume sage is in PATH."];
299
}
300
301
// Quit since there's no point going on.
302
// [NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0];
303
304
NSLog(@"WARNING: Could not find a good sage executable, falling back to sage and hoping it's in PATH.");
305
sageBinary = @"sage";
306
}
307
}
308
309
-(IBAction)revealInFinder:(id)sender{
310
if ( [[sender title] isEqualToString:@"Reveal in Shell"] ) {
311
[self terminalRun:[NSString stringWithFormat:@"cd '%@' && $SHELL",
312
[sageBinary stringByDeletingLastPathComponent]]];
313
} else {
314
[[NSWorkspace sharedWorkspace] selectFile:[sageBinary stringByDeletingLastPathComponent]
315
inFileViewerRootedAtPath:nil];
316
}
317
}
318
319
-(IBAction)openNotebook:(id)sender{
320
[self browseLocalSageURL:@""];
321
}
322
323
-(IBAction)newWorksheet:(id)sender{
324
[self browseLocalSageURL:@"new_worksheet"];
325
}
326
327
-(IBAction)showPreferences:(id)sender{
328
[NSApp activateIgnoringOtherApps:YES];
329
[prefWindow makeKeyAndOrderFront:self];
330
}
331
332
-(IBAction)browseLocalSageURL:(id)sender{
333
NSString *sageURL;
334
if ([sender isKindOfClass:[NSString class]]) {
335
sageURL = sender;
336
} else {
337
sageURL = [[defaults arrayForKey:@"sageURLs"] objectAtIndex:[sender tag]];
338
}
339
// The server is not running
340
if ( port == 0 && [defaults boolForKey:@"autoStartServer"] ) {
341
// Queue the URL up for opening and start the server
342
// Do I need to retain it??
343
[URLQueue addObject:sageURL];
344
[self startServer:self];
345
} else {
346
// Browse to the url right away
347
[self sageBrowse:[NSString stringWithFormat:@"http://localhost:%d/%@", port, sageURL]];
348
}
349
}
350
351
-(IBAction)browseRemoteURL:(id)sender{
352
NSString *sageURL;
353
if ([sender isKindOfClass:[NSString class]]) {
354
sageURL = sender;
355
} else {
356
sageURL = [[defaults arrayForKey:@"sageURLs"] objectAtIndex:[sender tag]];
357
}
358
[self sageBrowse:sageURL];
359
}
360
361
-(void)sageBrowse:(NSString*)location{
362
363
if ( !useSystemBrowser ) {
364
[[NSApplication sharedApplication] activateIgnoringOtherApps:TRUE];
365
366
NSError *outError = nil;
367
id myDocument = [[NSDocumentController sharedDocumentController]
368
openUntitledDocumentAndDisplay:YES error:&outError];
369
if ( myDocument == nil ) {
370
[NSApp presentError:outError];
371
NSLog(@"sageBrowser: Error creating document: %@", [outError localizedDescription]);
372
} else {
373
[[[myDocument webView] mainFrame]
374
loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:location]]];
375
}
376
377
} else if ( [defaults boolForKey:@"respectSAGE_BROWSER"] ) {
378
379
// TODO: escape quotes in location
380
NSString *command = [NSString
381
stringWithFormat:@"%@ -min -c 'import sage.misc.viewer as b; os.system(b.browser() + \" %@\")' &",
382
sageBinary,
383
location];
384
385
// TODO: Should probably make this use NSTask
386
system([command UTF8String]);
387
} else {
388
389
if ( [location characterAtIndex:0] == '/' ) {
390
[[NSWorkspace sharedWorkspace] openFile:location];
391
} else {
392
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:location]];
393
}
394
}
395
}
396
397
398
-(NSString*)convertMenuTitleToSageCommand:(NSString*)title{
399
400
if ( [title isEqualToString:@"Sage"] || [title isEqualToString:@"Sage (advanced)"] || [title isEqualToString:@"Terminal Session"] ) {
401
// A few special cases to open sage itself
402
return nil;
403
} else if ( ([title length] > 2) && [[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember:[title characterAtIndex:0]] ) {
404
// If it's capitalized, and more than one character then use the lowercased first letter.
405
// This is so things like Build and Test can work, but R and M2 will still work.
406
// This is really a hack, because I'm too lazy to create a bunch of different methods (and I think it's ugly)
407
unichar first = [[title lowercaseString] characterAtIndex:0];
408
return [NSString stringWithCharacters:&first length:1];
409
} else {
410
// If it's lowercased, assume it's the command, but remove ... from the end
411
return [title stringByTrimmingCharactersInSet:
412
[NSCharacterSet characterSetWithCharactersInString:
413
[NSString stringWithFormat:@"%C", ((unsigned short)0x2026)]]]; // @"…"
414
}
415
}
416
417
-(IBAction)terminalSession:(id)sender{
418
[self sageTerminalRun: [self convertMenuTitleToSageCommand:[sender title]] withArguments: nil];
419
}
420
421
-(IBAction)terminalSessionPromptForInput:(id)sender{
422
423
NSString *sessionType = [self convertMenuTitleToSageCommand:[sender title]];
424
NSString *command;
425
if ( [sessionType length] > 1 ) {
426
command = [sageBinary stringByAppendingFormat:@" --%@", sessionType];
427
} else if ( [sessionType length] > 0 ) {
428
command = [sageBinary stringByAppendingFormat:@" -%@", sessionType];
429
} else {
430
command = sageBinary;
431
}
432
433
[defaults synchronize];
434
NSString *defArgs = [[defaults dictionaryForKey:@"DefaultArguments"]
435
objectForKey:command];
436
437
[inputPanelController runCommand:command
438
withPrompt:[self createPrompt:sessionType forCommand:command]
439
withArguments:defArgs
440
editingCommand:[defaults boolForKey:@"editFullCommands"]];
441
}
442
443
-(NSString*)createPrompt:(NSString*)sessionType forCommand:(NSString*)command{
444
return [NSString stringWithFormat:@"Going to run sage %@\nPlease enter any arguments, escaped as you would for a shell.\n\nThe command will be run as\n%@ %C",
445
sessionType ? sessionType : @"", command, ((unsigned short)0x2026)];
446
}
447
448
-(IBAction)terminalSessionPromptForFile:(id)sender{
449
450
// Create a File Open Dialog class
451
NSOpenPanel *openDlg = [NSOpenPanel openPanel];
452
453
// Enable the selection of files and directories
454
[openDlg setCanChooseFiles:YES];
455
[openDlg setCanChooseDirectories:YES];
456
[openDlg setAllowsMultipleSelection:YES];
457
[openDlg setTitle:[NSString stringWithFormat:@"Choose file(s) for %@",[sender title]]];
458
459
// Display the dialog. If the OK button was pressed,
460
// process the files.
461
NSString * base_dir = nil;
462
if (neverOpenedFileBrowser) {
463
base_dir = [NSString stringWithFormat:@"%@/../devel/sage/sage",sageBinary];
464
neverOpenedFileBrowser=NO;
465
}
466
// If they supply files, then run the command
467
if ( [openDlg runModalForDirectory:base_dir file:nil] == NSOKButton ) {
468
[self sageTerminalRun:[self convertMenuTitleToSageCommand:[sender title]]
469
withArguments:[openDlg filenames]];
470
}
471
}
472
473
-(void)sageTerminalRun:(NSString*)sessionType withArguments:(NSArray*)arguments{
474
NSString *command;
475
if ( sessionType == nil ) {
476
NSLog(@"starting sage" );
477
command = sageBinary;
478
} else if ( [sessionType length] > 1 ) {
479
command = [sageBinary stringByAppendingFormat:@" --%@", sessionType];
480
} else {
481
command = [sageBinary stringByAppendingFormat:@" -%@", sessionType];
482
}
483
484
// Get any default options they might have for this session
485
[defaults synchronize];
486
NSString *defArgs = [[defaults dictionaryForKey:@"DefaultArguments"]
487
objectForKey:(sessionType != nil) ? sessionType : @"sage" ];
488
if ( defArgs != nil ) {
489
command = [command stringByAppendingFormat:@" %@", defArgs];
490
}
491
if ( arguments != nil ) {
492
for( int i = 0; i < [arguments count]; i++ ) {
493
command = [command stringByAppendingFormat:@" %@", [arguments objectAtIndex:i]];
494
}
495
}
496
497
// Hold command key to edit before running
498
if ( [defaults boolForKey:@"alwaysPromptForArguments"] || [[NSApp currentEvent] modifierFlags] & NSCommandKeyMask ) {
499
[inputPanelController runCommand:command
500
withPrompt:[self createPrompt:sessionType forCommand:command]
501
withArguments:defArgs
502
editingCommand:YES];
503
} else {
504
[self terminalRun:command];
505
}
506
}
507
508
-(void)terminalRun:(NSString*)command{
509
NSLog(@"Running command: %@", command);
510
511
// Escape quotes and backslashes in the command
512
// I think that's all we need to handle for applescript itself
513
NSMutableString * escapedCommand = [NSMutableString stringWithString:command];
514
[escapedCommand replaceOccurrencesOfString:@"\\"
515
withString:@"\\\\"
516
options:0
517
range:NSMakeRange(0, [escapedCommand length])];
518
[escapedCommand replaceOccurrencesOfString:@"\""
519
withString:@"\\\""
520
options:0
521
range:NSMakeRange(0, [escapedCommand length])];
522
// We can't use the (arguably easier) stringByReplacingOccurrencesOfString:withString since that's 10.5+
523
// NSString *escapedCommand = [[command stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]
524
// stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
525
526
// Which applescript to run
527
NSString *ApplescriptKey = [defaults objectForKey:@"TerminalEmulator"];
528
// Print the command into the applescript
529
NSString *bringAppToFrontScript =
530
[NSString stringWithFormat:[[defaults dictionaryForKey:@"TerminalEmulatorList"]
531
objectForKey:ApplescriptKey],
532
escapedCommand];
533
534
// NSLog(@"Executing applescript: %@", bringAppToFrontScript);
535
536
NSDictionary* errorDict;
537
NSAppleEventDescriptor *returnDescriptor = NULL;
538
NSAppleScript* scriptObject = [[NSAppleScript alloc]
539
initWithSource:bringAppToFrontScript];
540
returnDescriptor = [scriptObject executeAndReturnError: &errorDict];
541
if ( returnDescriptor == nil ) {
542
NSLog(@"terminalRun: Error running Applescript: %@", errorDict);
543
}
544
[scriptObject release];
545
}
546
547
// http://www.cocoadev.com/index.pl?DeterminingOSVersion
548
-(BOOL)isTigerOrLess{
549
OSErr err;
550
SInt32 version;
551
if ((err = Gestalt(gestaltSystemVersionMajor, &version)) != noErr) {
552
NSLog(@"Unable to determine gestaltSystemVersionMajor: %hd",err);
553
return YES;
554
}
555
if ( version < 10 ) return YES; // Of course this should never happen...
556
if ((err = Gestalt(gestaltSystemVersionMinor, &version)) != noErr) {
557
NSLog(@"Unable to determine gestaltSystemVersionMinor: %hd",err);
558
return YES;
559
}
560
if ( version < 5 ) return YES;
561
return NO;
562
}
563
564
// TODO: make installing packages easy -- stringByLaunchingPath:withArguments:error:
565
// TODO: maybe this should be written in py-objc so that we can call into sage directly (but then we would have to worry about environment etc.)
566
// TODO: make some services (search for NSSendTypes) -- pack/unpack spkg, extract sws from pdf, crap/fixdoctests/preparse/Test/coverage/pkg/pkg_nc/etc.
567
568
// TODO: open files such as .sws, .sage, .py, .spkg, -- .pdf (and extract sws from them), .htm, whatever else I can handle
569
// TODO: quicklook generator, spotlight importer -- use UTI
570
// NOTE: http://developer.apple.com/mac/library/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html
571
// TODO: icons for files -- they need some help with the alpha channel. I clearly don't know what I'm doing. I should really make them all from by script...
572
573
574
@end
575
576