diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0605e7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +*.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +# Haxe +.haxelib diff --git a/build.hxml b/build.hxml new file mode 100644 index 0000000..3c302a7 --- /dev/null +++ b/build.hxml @@ -0,0 +1,4 @@ +--interp +-main hxextern.Main +-cp src/ +-lib hxargs diff --git a/haxelib.json b/haxelib.json new file mode 100644 index 0000000..26227d3 --- /dev/null +++ b/haxelib.json @@ -0,0 +1,15 @@ +{ + "main": "hxextern.Main", + "name": "hxextern", + "url": "https://github.com/ExternKit/hxextern", + "license": "MIT", + "tags": ["extern", "hxextern"], + "description": "Extern manager", + "version": "0.1.0", + "classPath": "src/", + "contributors": ["fantoine"], + "dependencies": { + "hxargs": "", + "hxssl": "" + } +} diff --git a/src/hxextern/Cli.hx b/src/hxextern/Cli.hx new file mode 100644 index 0000000..cdb428e --- /dev/null +++ b/src/hxextern/Cli.hx @@ -0,0 +1,66 @@ +package hxextern; + +import hxargs.Args; +import hxextern.command.*; +import hxextern.service.*; + +typedef ArgsHandler = { + function getDoc() : String; + + function parse(__args : Array) : Void; +}; + +class Cli +{ + private var handler : ArgsHandler; + + public function new() + { + this.handler = Args.generate([ + @doc('List available externs') + 'list' => function(?target : String) : Void { + this.runCommand(new ListCommand(target)); + }, + + @doc('Search for an extern') + 'search' => function(name : String, ?target : String) : Void { + this.runCommand(new SearchCommand(name, target)); + }, + + @doc('Generate extern code') + 'generate' => function(?path : String) : Void { + trace('Not implemented'); + }, + + @doc('Show this help') + 'help' => function() : Void { + this.runCommand(new HelpCommand(this.handler.getDoc())); + }, + + _ => function(arg : String) : Void { + this.runCommand(new HelpCommand(this.handler.getDoc())); + }, + ]); + } + + public function run(args : Array) : Void + { + if (args.length == 0) { + args.push('help'); + } + try { + this.handler.parse(args); + } catch (e : Dynamic) { + Console.instance.error(Std.string(e)); + } + } + + private function runCommand(command : ICommand) : Void + { + try { + command.run(); + } catch (e : Dynamic) { + Console.instance.error(Std.string(e)); + } + } +} diff --git a/src/hxextern/Main.hx b/src/hxextern/Main.hx new file mode 100644 index 0000000..a7d3bb3 --- /dev/null +++ b/src/hxextern/Main.hx @@ -0,0 +1,15 @@ +package hxextern; + +class Main +{ + public static function main() : Void + { + // Get arguments + var args = Sys.args(); + args.pop(); + + // Run + var cli = new Cli(); + cli.run(args); + } +} diff --git a/src/hxextern/Target.hx b/src/hxextern/Target.hx new file mode 100644 index 0000000..4e342d4 --- /dev/null +++ b/src/hxextern/Target.hx @@ -0,0 +1,30 @@ +package hxextern; + +@:enum +abstract Target(String) to String +{ + var Cpp : Target = 'cpp'; + var Cs : Target = 'cs'; + var Flash : Target = 'flash'; + var Java : Target = 'java'; + var Js : Target = 'js'; + var Lua : Target = 'lua'; + var Neko : Target = 'neko'; + var Php : Target = 'php'; + var Python : Target = 'python'; + + public static function fromString(target : String) : Null + { + return switch (target.toLowerCase()) { + case 'cpp' | 'c++': Cpp; + case 'cs' | 'csharp': Cs; + case 'flash': Flash; + case 'js' | 'javascript': Js; + case 'lua': Lua; + case 'neko' | 'n': Neko; + case 'php': Php; + case 'py' | 'python': Python; + case _: throw 'Unsupported target "${target}"'; + } + } +} diff --git a/src/hxextern/command/AbstractListCommand.hx b/src/hxextern/command/AbstractListCommand.hx new file mode 100644 index 0000000..91b177d --- /dev/null +++ b/src/hxextern/command/AbstractListCommand.hx @@ -0,0 +1,32 @@ +package hxextern.command; + +import hxextern.service.Console; +import hxextern.service.Repository; + +class AbstractListCommand implements ICommand +{ + public function new() + { + // Nothing to do + } + + public function run() : Void + { + throw 'Must be overriden'; + } + + private function printList(infos : Array) : Void + { + if (0 == infos.length) { + Console.instance.info('No results found'); + return; + } + + Console.instance.success('Found ${infos.length} result(s)'); + for (info in infos) { + Console.instance.message(' * ', false); + Console.instance.info('${info.name} (${info.target})', false); + Console.instance.message(' : ${info.description}'); + } + } +} diff --git a/src/hxextern/command/HelpCommand.hx b/src/hxextern/command/HelpCommand.hx new file mode 100644 index 0000000..5e1b323 --- /dev/null +++ b/src/hxextern/command/HelpCommand.hx @@ -0,0 +1,16 @@ +package hxextern.command; + +class HelpCommand implements ICommand +{ + private var doc : String; + + public function new(doc : String) + { + this.doc = doc; + } + + public function run() : Void + { + Sys.println(this.doc); + } +} diff --git a/src/hxextern/command/ICommand.hx b/src/hxextern/command/ICommand.hx new file mode 100644 index 0000000..52e941b --- /dev/null +++ b/src/hxextern/command/ICommand.hx @@ -0,0 +1,6 @@ +package hxextern.command; + +interface ICommand +{ + public function run() : Void; +} diff --git a/src/hxextern/command/ListCommand.hx b/src/hxextern/command/ListCommand.hx new file mode 100644 index 0000000..7c1a596 --- /dev/null +++ b/src/hxextern/command/ListCommand.hx @@ -0,0 +1,23 @@ +package hxextern.command; + +import hxextern.service.Repository; + +class ListCommand extends AbstractListCommand +{ + private var target : String; + + public function new(?target : String) + { + super(); + + this.target = target; + } + + public override function run() : Void + { + var target = (null == this.target ? null : Target.fromString(this.target)); + + var infos = Repository.instance.list(target); + this.printList(infos); + } +} diff --git a/src/hxextern/command/SearchCommand.hx b/src/hxextern/command/SearchCommand.hx new file mode 100644 index 0000000..4d3690d --- /dev/null +++ b/src/hxextern/command/SearchCommand.hx @@ -0,0 +1,25 @@ +package hxextern.command; + +import hxextern.service.Repository; + +class SearchCommand extends AbstractListCommand +{ + private var name : String; + private var target : String; + + public function new(name : String, ?target : String) + { + super(); + + this.name = name; + this.target = target; + } + + public override function run() : Void + { + var target = (null == this.target ? null : Target.fromString(this.target)); + + var infos = Repository.instance.find(this.name, target); + this.printList(infos); + } +} diff --git a/src/hxextern/service/Console.hx b/src/hxextern/service/Console.hx new file mode 100644 index 0000000..8b3d5fc --- /dev/null +++ b/src/hxextern/service/Console.hx @@ -0,0 +1,65 @@ +package hxextern.service; + +import haxe.io.Output; +import hxextern.utils.Colors; + +class Console +{ + @:isVar + public static var instance(get, null) : Console; + private static function get_instance() : Console + { + if (null == Console.instance) { + Console.instance = new Console(); + } + return Console.instance; + } + + private var stdout : Output; + private var stderr : Output; + + private function new() + { + this.stdout = Sys.stdout(); + this.stderr = Sys.stderr(); + } + + public function message(message : String, newline : Bool = true) : Void + { + this.print(this.stdout, message, newline); + } + + public function debug(message : String, newline : Bool = true) : Void + { + this.print(this.stdout, Colors.magenta('${message}'), newline); + } + + public function info(message : String, newline : Bool = true) : Void + { + this.print(this.stdout, Colors.cyan('${message}'), newline); + } + + public function success(message : String, newline : Bool = true) : Void + { + this.print(this.stdout, Colors.green('${message}'), newline); + } + + public function warning(message : String, newline : Bool = true) : Void + { + this.print(this.stderr, Colors.yellow('${message}'), newline); + } + + public function error(message : String, newline : Bool = true) : Void + { + this.print(this.stderr, Colors.red('${message}'), newline); + } + + private function print(output : Output, message : String, newline : Bool = true) : Void + { + output.writeString(message); + if (newline) { + output.writeString('\n'); + } + output.flush(); + } +} diff --git a/src/hxextern/service/Process.hx b/src/hxextern/service/Process.hx new file mode 100644 index 0000000..3dfe597 --- /dev/null +++ b/src/hxextern/service/Process.hx @@ -0,0 +1,52 @@ +package hxextern.service; + +import haxe.io.Bytes; +import sys.io.Process as HxProcess; + +typedef ProcessOutput = { + var output : Null; + var error : Null; + var valid : Bool; + var code : Int; +}; + +class Process +{ + @:isVar + public static var instance(get, null) : Process; + private static function get_instance() : Process + { + if (null == Process.instance) { + Process.instance = new Process(); + } + return Process.instance; + } + + private function new() + { + + } + + public function execute(command : String, ?args : Array) : ProcessOutput + { + Console.instance.debug('Calling command "${command}' + (null != args && args.length > 0 ? ' ' + args.join(' ') : '') + '"'); + + var process = new HxProcess(command, args); + var output = process.stdout.readAll().toString(); + var error = process.stderr.readAll().toString(); + var result = ('' != error && null != error ? { + output: null, + error: error, + valid: false, + code: process.exitCode(), + } : { + output: output, + error: null, + valid: true, + code: process.exitCode(), + }); + process.close(); + + return result; + } +} diff --git a/src/hxextern/service/Repository.hx b/src/hxextern/service/Repository.hx new file mode 100644 index 0000000..936da29 --- /dev/null +++ b/src/hxextern/service/Repository.hx @@ -0,0 +1,108 @@ +package hxextern.service; + +import haxe.Json; +import hxextern.service.Process; +import hxextern.Target; + +typedef RepositoryInfo = { + var name : String; + var target : Target; + var description : String; + var url : String; +}; + +class Repository +{ + private static inline var REPOS_URL : String = 'https://api.github.com/orgs/ExternKit/repos'; + + private static var REPO_PATTERN : EReg = ~/extern-([a-z]+)-([a-z0-9_-]+)/ig; + + @:isVar + public static var instance(get, null) : Repository; + private static function get_instance() : Repository + { + if (null == Repository.instance) { + Repository.instance = new Repository(); + } + return Repository.instance; + } + + private var repositories : Array; + + public function new() + { + this.repositories = null; + } + + private function preload() : Void + { + if (null != this.repositories) { + return; + } + + this.repositories = []; + + // Get json from Github + var result = Process.instance.execute('curl', ['-sSf', Repository.REPOS_URL]); + if (!result.valid) { + throw result.error; + } + var json = Json.parse(result.output); + + // Extract informations from json + for (entry in (json : Array)) { + if (Repository.REPO_PATTERN.match(entry.name)) { + // Check if target is supported + var target = Target.fromString(Repository.REPO_PATTERN.matched(1)); + if (null == target) { + continue; + } + + // Extract informations + this.repositories.push({ + name: Repository.REPO_PATTERN.matched(2).toLowerCase(), + target: target, + description: entry.description, + url: entry.url + }); + } + } + + this.repositories.sort(function(info1 : RepositoryInfo, info2 : RepositoryInfo) : Int { + if (info1.target != info2.target) { + return Reflect.compare(info1.target, info2.target); + } + + return Reflect.compare(info1.name, info2.name); + }); + } + + public function list(?target : Target) : Array + { + this.preload(); + + if (null == target) { + return this.repositories.copy(); + } + + return [ + for (info in this.repositories) + if (info.target == target) + info + ]; + } + + public function find(name : String, ?target : Target) : Array + { + this.preload(); + + var escapedName = ~/([-[\]{}()*+?.,\\^$|#\s])/g.replace(name, '\\$1'); + var ereg = new EReg(escapedName, 'ig'); + + return [ + for (info in this.repositories) + if (ereg.match(info.name) && (null == target || info.target == target)) + info + ]; + } +} diff --git a/src/hxextern/utils/Colors.hx b/src/hxextern/utils/Colors.hx new file mode 100644 index 0000000..173b072 --- /dev/null +++ b/src/hxextern/utils/Colors.hx @@ -0,0 +1,57 @@ +package hxextern.utils; + +@:enum +abstract AnsiColor(String) to String { + var Black = '\033[0;30m'; + var Red = '\033[0;31m'; + var Green = '\033[0;32m'; + var Yellow = '\033[0;33m'; + var Blue = '\033[0;34m'; + var Magenta = '\033[0;35m'; + var Cyan = '\033[0;36m'; + var Gray = '\033[0;37m'; + var White = '\033[1;37m'; + var None = '\033[0;0m'; +} + +class Colors { + public static inline function red(input : String) : String { + return Colors.color(input, Red); + } + + public static inline function green(input : String) : String { + return Colors.color(input, Green); + } + + public static inline function yellow(input : String) : String { + return Colors.color(input, Yellow); + } + + public static inline function blue(input : String) : String { + return Colors.color(input, Blue); + } + + public static inline function magenta(input : String) : String { + return Colors.color(input, Magenta); + } + + public static inline function cyan(input : String) : String { + return Colors.color(input, Cyan); + } + + public static inline function gray(input : String) : String { + return Colors.color(input, Gray); + } + + public static inline function white(input : String) : String { + return Colors.color(input, White); + } + + public static inline function none(input : String) : String { + return Colors.color(input, None); + } + + public static inline function color(input : String, ansiColor : AnsiColor) : String { + return '${ansiColor}$input${AnsiColor.None}'; + } +}