diff --git a/Applications/TextMate/src/AppController.mm b/Applications/TextMate/src/AppController.mm index 5bc1ca0b..5b225a54 100644 --- a/Applications/TextMate/src/AppController.mm +++ b/Applications/TextMate/src/AppController.mm @@ -1,6 +1,7 @@ #import "AppController.h" #import "Favorites.h" #import "CreditsWindowController.h" +#import "InstallBundleItems.h" #import #import #import @@ -24,9 +25,15 @@ OAK_DEBUG_VAR(AppController); void OakOpenDocuments (NSArray* paths) { std::vector documents; + NSMutableArray* itemsToInstall = [NSMutableArray array]; for(NSString* path in paths) { - if(path::is_directory(to_s(path))) + static std::string const tmItemExtensions[] = { "tmbundle", "tmcommand", "tmdragcommand", "tmlanguage", "tmmacro", "tmplugin", "tmpreferences", "tmsnippet", "tmtheme" }; + if(oak::contains(beginof(tmItemExtensions), endof(tmItemExtensions), to_s([[path pathExtension] lowercaseString])) && !([NSEvent modifierFlags] & NSAlternateKeyMask)) + { + [itemsToInstall addObject:path]; + } + else if(path::is_directory(to_s(path))) { document::show_browser(to_s(path)); } @@ -36,6 +43,9 @@ void OakOpenDocuments (NSArray* paths) } } + if([itemsToInstall count]) + InstallBundleItems(itemsToInstall); + document::show(documents); } diff --git a/Applications/TextMate/src/InstallBundleItems.h b/Applications/TextMate/src/InstallBundleItems.h new file mode 100644 index 00000000..aaffb601 --- /dev/null +++ b/Applications/TextMate/src/InstallBundleItems.h @@ -0,0 +1,3 @@ +#import + +PUBLIC void InstallBundleItems (NSArray* itemPaths); diff --git a/Applications/TextMate/src/InstallBundleItems.mm b/Applications/TextMate/src/InstallBundleItems.mm new file mode 100644 index 00000000..0f0c723f --- /dev/null +++ b/Applications/TextMate/src/InstallBundleItems.mm @@ -0,0 +1,210 @@ +#import "InstallBundleItems.h" +#import +#import +#import +#import +#import +#import +#import + +static std::map installed_items () +{ + std::map res; + citerate(item, bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeAny, oak::uuid_t(), false, true)) + { + citerate(path, (*item)->paths()) + res.insert(std::make_pair(*path, *item)); + } + return res; +} + +void InstallBundleItems (NSArray* itemPaths) +{ + struct info_t + { + info_t (std::string const& path, std::string const& name, oak::uuid_t const& uuid, bool isBundle, bundles::item_ptr installed = bundles::item_ptr()) : path(path), name(name), uuid(uuid), is_bundle(isBundle), installed(installed) { } + + std::string path; + std::string name; + oak::uuid_t uuid; + bool is_bundle; + bundles::item_ptr installed; + }; + + std::map const installedItems = installed_items(); + std::vector installed, toInstall, delta, malformed; + + for(NSString* path in itemPaths) + { + bool isDelta; + std::string bundleName; + oak::uuid_t bundleUUID; + bundles::item_ptr installedItem; + + bool isBundle = [[[path pathExtension] lowercaseString] isEqualToString:@"tmbundle"]; + std::string const loadPath = isBundle ? path::join(to_s(path), "info.plist") : to_s(path); + plist::dictionary_t const infoPlist = plist::load(loadPath); + + auto it = installedItems.find(loadPath); + if(it != installedItems.end()) + installedItem = it->second; + + if(plist::get_key_path(infoPlist, "isDelta", isDelta) && isDelta) + { + delta.push_back(info_t(to_s(path), NULL_STR, oak::uuid_t(), isBundle, installedItem)); + } + else if(plist::get_key_path(infoPlist, "name", bundleName) && plist::get_key_path(infoPlist, "uuid", bundleUUID)) + { + if(installedItem) + installed.push_back(info_t(to_s(path), bundleName, bundleUUID, isBundle, installedItem)); + else toInstall.push_back(info_t(to_s(path), bundleName, bundleUUID, isBundle, installedItem)); + } + else + { + malformed.push_back(info_t(to_s(path), NULL_STR, oak::uuid_t(), isBundle, installedItem)); + } + } + + iterate(info, delta) + { + char const* type = info->is_bundle ? "bundle" : "bundle item"; + std::string const name = path::name(path::strip_extension(info->path)); + std::string const title = text::format("The %s “%s” could not be installed because it is in delta format.", type, name.c_str()); + NSRunAlertPanel([NSString stringWithCxxString:title], @"Contact the author of this %s to get a properly exported version.", @"OK", nil, nil, type); + } + + iterate(info, malformed) + { + char const* type = info->is_bundle ? "bundle" : "bundle item"; + std::string const name = path::name(path::strip_extension(info->path)); + std::string const title = text::format("The %s “%s” could not be installed because it is malformed.", type, name.c_str()); + NSRunAlertPanel([NSString stringWithCxxString:title], @"The %s lacks mandatory keys in its property list file.", @"OK", nil, nil, type); + } + + iterate(info, installed) + { + char const* type = info->is_bundle ? "bundle" : "bundle item"; + std::string const name = info->name; + std::string const title = text::format("The %s “%s” is already installed.", type, name.c_str()); + int choice = NSRunAlertPanel([NSString stringWithCxxString:title], @"You can edit the installed %s to inspect it.", @"OK", @"Edit", nil, type); + if(choice == NSAlertAlternateReturn) // "Edit" + [[BundleEditor sharedInstance] revealBundleItem:info->installed]; + } + + iterate(info, toInstall) + { + if(info->is_bundle) + { + int choice = NSRunAlertPanel([NSString stringWithFormat:@"Would you like to install the “%@” bundle?", [NSString stringWithCxxString:info->name]], @"Installing a bundle adds new functionality to TextMate.", @"Install", @"Cancel", nil); + if(choice == NSAlertDefaultReturn) // "Install" + { + std::string const installDir = path::join(path::home(), "Library/Application Support/Avian/Pristine Copy/Bundles"); + if(path::make_dir(installDir)) + { + std::string const installPath = path::unique(path::join(installDir, path::name(info->path))); + if(path::copy(info->path, installPath)) + { + fprintf(stderr, "installed bundle at: %s\n", installPath.c_str()); + continue; + } + } + fprintf(stderr, "failed to install bundle: %s\n", info->path.c_str()); + } + } + else + { + oak::uuid_t defaultBundle; + if(path::extension(info->path) == ".tmTheme") + { + citerate(item, bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeBundle, "A4380B27-F366-4C70-A542-B00D26ED997E")) + defaultBundle = (*item)->uuid(); + } + + std::map vars; + vars.insert(std::make_pair("TM_FULLNAME", getpwuid(getuid())->pw_gecos ?: "John Doe")); + std::string personalBundleName = format_string::expand("${TM_FULLNAME/^(\\S+).*$/$1/}’s Bundle", vars); + // std::string personalBundleName = format_string::expand("${TM_FULLNAME/^(\\S+).*$/$1/}’s Bundle", std::map{ { "TM_FULLNAME", getpwuid(getuid())->pw_gecos ?: "John Doe" } }); + if(!defaultBundle) + { + citerate(item, bundles::query(bundles::kFieldName, personalBundleName, scope::wildcard, bundles::kItemTypeBundle)) + defaultBundle = (*item)->uuid(); + } + + NSPopUpButton* bundleChooser = [[[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO] autorelease]; + [bundleChooser.menu removeAllItems]; + [bundleChooser.menu addItemWithTitle:@"Create new bundle…" action:NULL keyEquivalent:@""]; + [bundleChooser.menu addItem:[NSMenuItem separatorItem]]; + + std::multimap ordered; + citerate(item, bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeBundle)) + ordered.insert(std::make_pair((*item)->name(), *item)); + NSMenuItem* selectedItem = nil; + iterate(pair, ordered) + { + NSMenuItem* menuItem = [bundleChooser.menu addItemWithTitle:[NSString stringWithCxxString:pair->first] action:NULL keyEquivalent:@""]; + [menuItem setRepresentedObject:[NSString stringWithCxxString:to_s(pair->second->uuid())]]; + if(defaultBundle && defaultBundle == pair->second->uuid()) + selectedItem = menuItem; + } + if(selectedItem) + [bundleChooser selectItem:selectedItem]; + + [bundleChooser sizeToFit]; + NSRect frame = [bundleChooser frame]; + if(NSWidth(frame) > 200) + [bundleChooser setFrameSize:NSMakeSize(200, NSHeight(frame))]; + + NSAlert* alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"Would you like to install the “%@” bundle item?", [NSString stringWithCxxString:info->name]] defaultButton:@"Install" alternateButton:@"Cancel" otherButton:nil informativeTextWithFormat:@"Installing a bundle item adds new functionality to TextMate."]; + [alert setAccessoryView:bundleChooser]; + if([alert runModal] == NSAlertDefaultReturn) // "Install" + { + static struct { std::string extension; std::string directory; } DirectoryMap[] = + { + { ".tmCommand", "Commands" }, + { ".tmDragCommand", "DragCommands" }, + { ".tmMacro", "Macros" }, + { ".tmPreferences", "Preferences" }, + { ".tmSnippet", "Snippets" }, + { ".tmLanguage", "Syntaxes" }, + { ".tmProxy", "Proxies" }, + { ".tmTheme", "Themes" }, + }; + + if(NSString* bundleUUID = [[bundleChooser selectedItem] representedObject]) + { + citerate(item, bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeBundle, to_s(bundleUUID))) + { + if((*item)->local() || (*item)->save()) + { + std::string dest = path::parent((*item)->paths().front()); + iterate(iter, DirectoryMap) + { + if(path::extension(info->path) == iter->extension) + { + dest = path::join(dest, iter->directory); + if(path::make_dir(dest)) + { + dest = path::join(dest, path::name(info->path)); + dest = path::unique(dest); + if(path::copy(info->path, dest)) + break; + else fprintf(stderr, "error: copy(‘%s’, ‘%s’)\n", info->path.c_str(), dest.c_str()); + } + else + { + fprintf(stderr, "error: makedir(‘%s’)\n", dest.c_str()); + } + } + } + + } + } + } + else + { + fprintf(stderr, "Create new bundle for item\n"); + } + } + } + } +}