mirror of
https://github.com/textmate/textmate.git
synced 2026-04-06 03:01:29 -04:00
382 lines
13 KiB
C++
382 lines
13 KiB
C++
#include <updater/updater.h>
|
||
#include <regexp/format_string.h>
|
||
#include <regexp/glob.h>
|
||
#include <oak/oak.h>
|
||
#include <text/case.h>
|
||
#include <text/ctype.h>
|
||
#include <text/decode.h>
|
||
#include <text/format.h>
|
||
#include <io/io.h>
|
||
#include <OakSystem/application.h>
|
||
|
||
static double const AppVersion = 1.3;
|
||
static size_t const AppRevision = APP_REVISION;
|
||
|
||
// example: bl install Apache AppleScript Blogging Bundle\ Development C CSS Diff Git HTML Hyperlink\ Helper JavaScript Mail Make Markdown Math Objective-C PHP Perl Property\ List Ragel Remind Ruby SQL Shell\ Script Source Subversion TODO Text TextMate XML Xcode
|
||
|
||
static int get_width ()
|
||
{
|
||
if(!isatty(STDIN_FILENO))
|
||
return INT_MAX;
|
||
|
||
struct winsize ws;
|
||
if(ioctl(0, TIOCGWINSZ, &ws) == -1)
|
||
{
|
||
perror("TIOCGWINSZ");
|
||
exit(EX_OSERR);
|
||
}
|
||
return ws.ws_col;
|
||
}
|
||
|
||
static std::string textify (std::string str)
|
||
{
|
||
str = format_string::replace(str, "\\A\\s+|<[^>]*>|\\s+\\z", "");
|
||
str = format_string::replace(str, "\\s+", " ");
|
||
str = decode::entities(str);
|
||
return str;
|
||
}
|
||
|
||
extern char* optarg;
|
||
extern int optind;
|
||
|
||
static void usage (FILE* io = stdout)
|
||
{
|
||
fprintf(io, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
|
||
fprintf(io, "Usage: %s list [Cs] [«bundle» ..]\n", getprogname());
|
||
fprintf(io, " %s install [Cs] «bundle» ..\n", getprogname());
|
||
fprintf(io, " %s uninstall [Cs] «bundle» ..\n", getprogname());
|
||
fprintf(io, " %s show [C] «bundle» ..\n", getprogname());
|
||
fprintf(io, " %s dependencies [Cs] [«bundle» ..]\n", getprogname());
|
||
fprintf(io, " %s dependents [C] [«bundle» ..]\n", getprogname());
|
||
fprintf(io, " %s update [C]\n", getprogname());
|
||
fprintf(io, " %s help\n", getprogname());
|
||
fprintf(io, "\n");
|
||
fprintf(io, "Options:\n");
|
||
fprintf(io, " -h/--help Show this help.\n");
|
||
fprintf(io, " -v/--version Show version number.\n");
|
||
fprintf(io, " -C/--directory «path» Use «path» for local bundles.\n");
|
||
fprintf(io, " -s/--source «name» Restrict bundles to the given source. Multiple sources are allowed.\n");
|
||
fprintf(io, "\n");
|
||
fprintf(io, "Globs: Both source and bundle names can contain wild cards:\n");
|
||
fprintf(io, " ? Match any single character\n");
|
||
fprintf(io, " * Match any characters\n");
|
||
fprintf(io, " {«a»,«b»,«c»} Match «a», «b», or «c».\n");
|
||
fprintf(io, "\n");
|
||
fprintf(io, "Special bundle names:\n");
|
||
fprintf(io, " outdated Bundles with a pending update\n");
|
||
fprintf(io, " installed Bundles already installed\n");
|
||
fprintf(io, " defaults Default bundles (excl. mandatories)\n");
|
||
fprintf(io, " mandatories Mandatory bundles\n");
|
||
}
|
||
|
||
static bool matches (std::string const& str, std::vector<std::string> const& globs)
|
||
{
|
||
if(globs.empty())
|
||
return true;
|
||
|
||
for(auto const& glob : globs)
|
||
{
|
||
if(path::glob_t(glob).does_match(str))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
static bool matches (bundles_db::bundle_ptr bundle, std::vector<std::string> const& globs)
|
||
{
|
||
if(globs.empty())
|
||
return true;
|
||
|
||
for(auto const& glob : globs)
|
||
{
|
||
if(glob == "outdated" && bundle->has_update())
|
||
return true;
|
||
if(glob == "installed" && bundle->installed())
|
||
return true;
|
||
if(glob == "defaults" && bundle->is_default())
|
||
return true;
|
||
if(glob == "mandatories" && bundle->is_mandatory())
|
||
return true;
|
||
if(path::glob_t(glob).does_match(text::lowercase(bundle->name())))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
static std::vector<bundles_db::bundle_ptr> filtered_bundles (std::vector<bundles_db::bundle_ptr> const& index, std::vector<std::string> const& sourceNames, std::vector<std::string> const& bundleNames)
|
||
{
|
||
std::vector<bundles_db::bundle_ptr> res;
|
||
std::set<oak::uuid_t> seen;
|
||
for(auto const& bundle : index)
|
||
{
|
||
if(matches(bundle->source() ? bundle->source()->identifier() : NULL_STR, sourceNames) && matches(bundle, bundleNames))
|
||
{
|
||
if(seen.find(bundle->uuid()) == seen.end())
|
||
{
|
||
seen.insert(bundle->uuid());
|
||
res.push_back(bundle);
|
||
}
|
||
}
|
||
}
|
||
return res;
|
||
}
|
||
|
||
static std::string short_bundle_info (bundles_db::bundle_ptr bundle, int width)
|
||
{
|
||
int descWidth = std::max(10, width - 23);
|
||
char status = bundle->installed() ? (bundle->has_update() ? 'U' : 'I') : ' ';
|
||
if(bundle->is_dependency())
|
||
status = tolower(status);
|
||
std::string desc = bundle->description();
|
||
desc = desc == NULL_STR ? "(no description)" : textify(desc);
|
||
if(desc.size() > descWidth)
|
||
desc.resize(descWidth);
|
||
return text::format("%c %-20.20s %s", status, bundle->name().c_str(), desc.c_str());
|
||
}
|
||
|
||
static void version ()
|
||
{
|
||
fprintf(stdout, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
|
||
}
|
||
|
||
int main (int argc, char const* argv[])
|
||
{
|
||
oak::application_t::set_support(path::join(path::home(), "Library/Application Support/TextMate"));
|
||
oak::application_t app(argc, argv);
|
||
|
||
static struct option const longopts[] = {
|
||
{ "directory", required_argument, 0, 'C' },
|
||
{ "source", required_argument, 0, 's' },
|
||
{ "help", no_argument, 0, 'h' },
|
||
{ "version", no_argument, 0, 'v' },
|
||
{ 0, 0, 0, 0 }
|
||
};
|
||
|
||
std::vector<std::string> sourceNames;
|
||
std::string installDir = NULL_STR;
|
||
|
||
unsigned int ch;
|
||
while((ch = getopt_long(argc, (char* const*)argv, "s:C:hv", longopts, NULL)) != -1)
|
||
{
|
||
switch(ch)
|
||
{
|
||
case 'C': installDir = optarg; break;
|
||
case 's': sourceNames.push_back(optarg); break;
|
||
case 'v': version(); return EX_OK;
|
||
case 'h': usage(); return EX_OK;
|
||
case '?': /* unknown option */ return EX_USAGE;
|
||
case ':': /* missing option */ return EX_USAGE;
|
||
default: usage(stderr); return EX_USAGE;
|
||
}
|
||
}
|
||
|
||
if(optind == argc)
|
||
return usage(stderr), EX_USAGE;
|
||
|
||
std::string command = argv[optind];
|
||
if(command == "help")
|
||
return usage(stdout), EX_OK;
|
||
|
||
std::vector<std::string> bundleNames;
|
||
for(int i = optind + 1; i < argc; ++i)
|
||
bundleNames.push_back(text::lowercase(argv[i]));
|
||
|
||
static std::set<std::string> const CommandsNeedingBundleList = { "install", "uninstall", "show", "dependents" };
|
||
if(bundleNames.empty() && CommandsNeedingBundleList.find(command) != CommandsNeedingBundleList.end())
|
||
{
|
||
fprintf(stderr, "no bundles specified\n");
|
||
return EX_USAGE;
|
||
}
|
||
|
||
static std::set<std::string> const CommandsNeedingUpdatedSources = { "update", "install", "list", "show" };
|
||
if(CommandsNeedingUpdatedSources.find(command) != CommandsNeedingUpdatedSources.end())
|
||
{
|
||
std::vector<bundles_db::source_ptr> toUpdate;
|
||
for(auto const& source : bundles_db::sources(installDir))
|
||
{
|
||
if(!source->disabled() && source->needs_update())
|
||
toUpdate.push_back(source);
|
||
}
|
||
|
||
__block std::vector<bundles_db::source_ptr> failedUpdate;
|
||
dispatch_apply(toUpdate.size(), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i){
|
||
fprintf(stderr, "Downloading ‘%s’…\n", toUpdate[i]->url().c_str());
|
||
if(!update(toUpdate[i]))
|
||
failedUpdate.push_back(toUpdate[i]);
|
||
});
|
||
|
||
for(auto source : failedUpdate)
|
||
fprintf(stderr, "*** failed to update source: ‘%s’ (%s)\n", source->name().c_str(), source->url().c_str());
|
||
|
||
if(!failedUpdate.empty())
|
||
exit(EX_UNAVAILABLE);
|
||
}
|
||
|
||
std::vector<bundles_db::bundle_ptr> index = bundles_db::index(installDir);
|
||
if(command == "list")
|
||
{
|
||
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
|
||
fprintf(stdout, "%s\n", short_bundle_info(bundle, get_width()).c_str());
|
||
}
|
||
else if(command == "install")
|
||
{
|
||
std::vector<bundles_db::bundle_ptr> toInstall;
|
||
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
|
||
{
|
||
if(!bundle->installed())
|
||
toInstall.push_back(bundle);
|
||
else fprintf(stderr, "skip ‘%s’ (%s) -- already installed\n", bundle->name().c_str(), bundle->origin().c_str());
|
||
}
|
||
|
||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||
size_t n = toInstall.size();
|
||
__block std::vector<bundles_db::bundle_ptr> failed;
|
||
__block std::vector<double> progress(n);
|
||
|
||
if(isatty(STDERR_FILENO))
|
||
{
|
||
if(dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()))
|
||
{
|
||
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, NSEC_PER_SEC / 10, NSEC_PER_SEC / 5);
|
||
dispatch_source_set_event_handler(timer, ^{
|
||
double current = 0, total = 0;
|
||
for(size_t i = 0; i < n; ++i)
|
||
{
|
||
current += toInstall[i]->size() * progress[i];
|
||
total += toInstall[i]->size();
|
||
}
|
||
fprintf(stderr, "\rDownloading... %4.1f%%", 100 * current / total);
|
||
});
|
||
dispatch_resume(timer);
|
||
}
|
||
}
|
||
|
||
dispatch_apply(n, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i){
|
||
if(!install(toInstall[i], installDir, &progress[i]))
|
||
failed.push_back(toInstall[i]);
|
||
});
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
if(isatty(STDERR_FILENO))
|
||
fprintf(stderr, "\rDownloading... Done! \n");
|
||
if(!failed.empty())
|
||
{
|
||
std::vector<std::string> names;
|
||
std::transform(failed.begin(), failed.end(), back_inserter(names), [](bundles_db::bundle_ptr const& bundle){ return bundle->name(); });
|
||
fprintf(stderr, "*** failed to install %s.\n", text::join(names, ", ").c_str());
|
||
}
|
||
save_index(index, installDir);
|
||
if(!failed.empty())
|
||
exit(EX_UNAVAILABLE);
|
||
exit(EX_OK);
|
||
});
|
||
});
|
||
dispatch_main();
|
||
}
|
||
else if(command == "uninstall")
|
||
{
|
||
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
|
||
{
|
||
if(bundle->installed())
|
||
{
|
||
fprintf(stderr, "Uninstalling ‘%s’...", bundle->name().c_str());
|
||
if(uninstall(bundle, installDir))
|
||
fprintf(stderr, "ok!\n");
|
||
else fprintf(stderr, " *** failed!\n");
|
||
}
|
||
else
|
||
{
|
||
fprintf(stderr, "skip ‘%s’ (%s) -- not installed\n", bundle->name().c_str(), bundle->origin().c_str());
|
||
}
|
||
}
|
||
save_index(index, installDir);
|
||
}
|
||
else if(command == "show")
|
||
{
|
||
bool first = true;
|
||
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
|
||
{
|
||
if(!std::exchange(first, false))
|
||
fprintf(stdout, "\n");
|
||
|
||
fprintf(stdout, "name: %s\n", bundle->name().c_str());
|
||
fprintf(stdout, "source: %s\n", bundle->source() ? bundle->source()->identifier().c_str() : "«no remote source»");
|
||
fprintf(stdout, "uuid: %s\n", to_s(bundle->uuid()).c_str());
|
||
|
||
bool hasName = bundle->contact_name() != NULL_STR;
|
||
bool hasEmail = bundle->contact_email() != NULL_STR;
|
||
char const* fmt[] = { "", "contact: %1$s\n", "contact: <%2$s>\n\0%1$s", "contact: %1$s <%2$s>\n" };
|
||
fprintf(stdout, fmt[(hasName ? 1 : 0) + (hasEmail ? 2 : 0)], bundle->contact_name().c_str(), bundle->contact_email().c_str());
|
||
|
||
if(bundle->url_updated())
|
||
fprintf(stdout, "date: %s\n", to_s(bundle->url_updated()).c_str());
|
||
if(bundle->url() != NULL_STR)
|
||
fprintf(stdout, "url: %s (%d bytes)\n", bundle->url().c_str(), bundle->size());
|
||
if(bundle->path() != NULL_STR)
|
||
fprintf(stdout, "path: %s\n", path::with_tilde(bundle->path()).c_str());
|
||
if(bundle->origin() != NULL_STR)
|
||
fprintf(stdout, "origin: %s\n", bundle->origin().c_str());
|
||
|
||
for(auto const& grammar : bundle->grammars())
|
||
{
|
||
std::vector<std::string> fileTypes;
|
||
for(auto const& ext : grammar->file_types())
|
||
fileTypes.push_back(ext);
|
||
if(grammar->mode_line() != NULL_STR)
|
||
fileTypes.push_back(text::format("/%s/", grammar->mode_line().c_str()));
|
||
if(fileTypes.empty())
|
||
fileTypes.push_back(grammar->scope());
|
||
fprintf(stdout, "grammar: %s (%s)\n", grammar->name().c_str(), text::join(fileTypes, ", ").c_str());
|
||
}
|
||
|
||
std::vector<std::string> dependencies;
|
||
for(auto const& dependency : bundle->dependencies(index))
|
||
dependencies.push_back(dependency->name());
|
||
if(!dependencies.empty())
|
||
fprintf(stderr, "dependencies: %s\n", text::join(dependencies, ", ").c_str());
|
||
|
||
if(bundle->description() != NULL_STR)
|
||
fprintf(stdout, "description: %s\n", textify(bundle->description()).c_str());
|
||
}
|
||
}
|
||
else if(command == "update")
|
||
{
|
||
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
|
||
{
|
||
if(bundle->has_update())
|
||
{
|
||
fprintf(stderr, "Updating ‘%s’...", bundle->name().c_str());
|
||
if(update(bundle, installDir))
|
||
fprintf(stderr, "ok!\n");
|
||
else fprintf(stderr, " *** failed!\n");
|
||
}
|
||
}
|
||
save_index(index, installDir);
|
||
}
|
||
else if(command == "dependencies")
|
||
{
|
||
std::vector<bundles_db::bundle_ptr> bundles;
|
||
for(auto const& bundle : filtered_bundles(index, sourceNames, bundleNames))
|
||
{
|
||
if(!bundleNames.empty() || bundle->installed())
|
||
bundles.push_back(bundle);
|
||
}
|
||
|
||
for(auto const& bundle : dependencies(index, bundles, false))
|
||
fprintf(stdout, "%s\n", short_bundle_info(bundle, get_width()).c_str());
|
||
}
|
||
else if(command == "dependents")
|
||
{
|
||
for(auto const& bundle : dependents(index, filtered_bundles(index, sourceNames, bundleNames)))
|
||
fprintf(stdout, "%s\n", short_bundle_info(bundle, get_width()).c_str());
|
||
}
|
||
else
|
||
{
|
||
fprintf(stderr, "unknown command: %s\n", command.c_str());
|
||
return EX_USAGE;
|
||
}
|
||
return EX_OK;
|
||
}
|