Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Everything Titanium using the CLI

Everything Titanium using the CLI

Titanium's new CLI has got some sugar from Appcelerator's team, and is now a very powerful and extensible tool. Indeed, it has become a sort of Swiss knife for the Titanium developer, allowing to automate various tasks : sdk install / upgrade, project build / deployment, translatable strings extraction, etc. Apart from the official CLI tool, there are numerous other useful tools : alloy, gittio, etc. Even more, it is possible to hack most of these tools to extend their capacities through the commands concept.

This talk, given during the TiConf Amsterdam 2014 (http://ticonf.org), showcases a lot of command line tools for productive Titanium developers, and explains how to create custom commands.

Xavier Lacot

June 29, 2014
Tweet

More Decks by Xavier Lacot

Other Decks in Programming

Transcript

  1. TiConf Amsterdam - Xavier Lacot - June 29th, 2014 Everything

    Titanium using the CLI
  2. Hello, I am Xavier • Founder, Web- and Mobile- expert

    at JoliCode • Contributor to several Open Source projects • Titanium developer since 2009 • Speaker at CodeStrong and TiConf • Former President of the French Association of PHP Users • Co-organizer of the Titanium Paris meetup group @xavierlacot with TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 2
  3. None
  4. A (brief) history as a Titanium developer...

  5. Titanium developer: 2009-2011 TiConf 2014 - Monitor Your App: A

    Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 5
  6. Titanium Studio: 2011-present TiConf 2014 - Monitor Your App: A

    Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 6
  7. My experience aside Titanium • Symfony developer since a lot

    of years • nodejs developer more recently • used to use Command Line Interfaces, aka. CLI TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 7
  8. The day when Titanium got his very own CLI TiConf

    2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 8
  9. CLI > GUI • a long debate... • Pros: •

    higher productivity • automate/script tasks • use the convenient tools rather than a "all-in-one" solution • My CLI is 1000% faster than your Eclipse crap ( Appcelerator) • feel like a hipster-hacker when it works • Cons: not polished and well packaged TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 9
  10. GUI CLI TiConf 2014 - Monitor Your App: A Complete

    Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 10
  11. The Titanium CLI...

  12. Get / install the tool • Installing the CLI is

    an easy task: $  npm  install  -­‐g  titanium • Want to live dangerously? $  npm  install  -­‐g  git://github.com/appcelerator/titanium.git TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 12
  13. ticonf-2014$ ^[p $ titanium sdk • $ ti sdk helps

    manage the installed Titanium sdk: • list : list installed sdks • install : (un-)install a sdk version / update to newest • select : select a specific version by default TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 13
  14. $ titanium config • ti config : list all options

    • ti config user.email : retrieves the value of a property • ti config user.email "hello@ticonf.org" : sets property Some interesting configuration options • cli.completion : enable cli tab-completion (working soon) • genymotion.enabled : enabled to push directly to genymotion without manually calling "adb" TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 14
  15. These settings are stored in ~/.titanium/config.json {   "user":  {

        "name":  "Xavier  Lacot",     "email":  "xavier@lacot.org"   },   "app":  {     "workspace":  "/Users/xavier/Documents/workspace/titanium",     "idprefix":  "com.jolicode",   },   [...],   "sdk":  {     "selected":  "3.2.3.GA"   },   "genymotion":  {     "enabled":  true   } } TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 15
  16. ticonf-2014$ ^[p $ titanium create • A Swiss tool for

    creating projects! • interactive: asks common questions (id, name, platforms, etc.) • fast: create a project in 10 seconds! • lightweight: it uses a simple two-tabs project template TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 16
  17. TiConf 2014 - Monitor Your App: A Complete Panel of

    Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 17
  18. Troubleshouting the configuration • $ ti setup check : checks

    the configuration TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 18
  19. Troubleshouting the configuration • $ ti setup quick : complete

    setup (developer name, sdk version, etc.) • $ ti setup app : default app values (company app id prefix, publisher, website, etc.) • other commands: user , network , cli , sdk , ios , android TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 19
  20. ticonf-2014$ ^[p $ titanium build • all platforms are supported

    • simulators or real devices • even store-submission is possible! $  ti  build  -­‐p  ios $  ti  build  -­‐p  android  -­‐T  dist-­‐playstore  -­‐K  jolicode.keystore  -­‐L  jolicode TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 20
  21. A lot of other commands... • $ ti status displays

    the current login/user status • $ ti info displays a complete diagnosis of the platform: • OS, JAVA, XCode • Titanium SDKs • Android SDK, Platforms and Emulators • Genymotion Emulators • iOS certificates and provisionnings, simulators, connected devices • etc. TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 21
  22. Extending the CLI

  23. Built extensible through plugins • add new commands • hook

    into exisiting commands TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 23
  24. Creating new commands 1. a single js file, where you

    want /path/to/sthing/pony.js 2. register it for Titanium's cli: $  ti  config  -­‐a  paths.commands  /path/to/sthing (adds /path/to/sthing to the paths.commands config array) 3. write the content of the command • a declarative part • command description, name, etc. • optionnaly, a config() method to set the default value of some execution option TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 24
  25. Creating new commands • the content of the command •

    a validate() method called to validate the options passed to the command • a run() method, the "body" of the command • the command name is the filename • pony for /path/to/sthing/pony.js • not possible to rename/alias it - ticket + PR on their way :-) • the latter loaded takes precedence the last pony command overrides an earlier pony TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 25
  26. A sample custom command exports.cliVersion  =  '>=3.2'; exports.name  =  'pony:fly';

    exports.desc  =  'makes  ponies  fly'; exports.config  =  function(logger,  config,  cli)  {        return  {                noAuth:  true,                skipBanner:  true,                options:  {                        quantity:  {                                abbr:  'qty',                                default:  1,                                desc:  'number  of  ponies  flying'                        }                }        }; };  ​  ​  ​ 1 3 TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 26
  27. A sample custom command exports.cliVersion  =  '>=3.2'; exports.name  =  'pony:fly';

    exports.desc  =  'makes  ponies  fly'; exports.config  =  function(logger,  config,  cli)  {        return  {                noAuth:  true,                skipBanner:  true,                options:  {                        quantity:  {                                abbr:  'qty',                                default:  1,                                desc:  'number  of  ponies  flying'                        }                }        }; };  ​  ​  ​  ​  ​  ​  ​  ​  ​  ​  ​  ​  ​ 5 17 TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 27
  28. A sample custom command exports.validate  =  function  (logger,  config,  cli)

     {        if  (cli.argv.quantity)  {                if  (!/^\d+$/.test(cli.argv.quantity))  {                        logger.banner();                        logger.error('quantity  must  be  an  integer\n');                        process.exit(1);                }        } }; exports.run  =  function  (logger,  config,  cli,  finished)  {        var  i  =  0;        while  (i  <  cli.argv.quantity)  {                console.log('ponies  are  flying!');                i++;        } };  ​  ​  ​  ​  ​  ​  ​  ​  ​ 1 9 TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 28
  29. A sample custom command exports.validate  =  function  (logger,  config,  cli)

     {        if  (cli.argv.quantity)  {                if  (!/^\d+$/.test(cli.argv.quantity))  {                        logger.banner();                        logger.error('quantity  must  be  an  integer\n');                        process.exit(1);                }        } }; exports.run  =  function  (logger,  config,  cli,  finished)  {        var  i  =  0;        while  (i  <  cli.argv.quantity)  {                console.log('ponies  are  flying!');                i++;        } };  ​  ​  ​  ​  ​  ​  ​  ​ 11 18 TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 29
  30. ticonf-2014$ ^[p A sample custom command TiConf 2014 - Monitor

    Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 30
  31. Hooking into commands: concept • idea: run operations when certain

    events occur • hooks are a common concept, not specific to Titanium • can somehow be seen as "events" at the cli scale TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 31
  32. Hooking into commands: howto 1. a single js file, where

    you want /path/to/sthing/soundAfterBuild.js 2. register it for Titanium's cli: $  ti  config  -­‐a  paths.hooks  /path/to/sthing (adds /path/to/sthing to the paths.hooks config array) 3. write the content of the hook • using cli.on() , attach listeners to the occurence of hooks • cli.addHook() is an alias for cli.on() TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 32
  33. ticonf-2014$ ^[p A sample custom hook var  path  =  require('path'),

      play  =  require('play'); exports.cliVersion  =  '>=3.2'; exports.init  =  function(logger,  config,  cli,  nodeappc)  {        cli.on('build.post.compile',  function(builder,  next)  {                play.sound(path.resolve(__dirname,  'sounds',  'trumpet.wav'));                next();        }) };  ​ 6 TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 33
  34. • build.pre.construct • build.pre.compile • build.android.startEmulator • build.android.copyResource • build.android.titaniumprep

    • build.android.writeAndroidManifest • build.android.aapt • build.android.javac • build.android.proguard • build.android.dexer • build.android.jarsigner • build.android.zipalign • build.ios.copyResource • build.ios.prerouting • build.ios.titaniumprep • build.ios.xcodebuild • build.ios.writeBuildManifest • build.post.compile • build.finalize • create.pre • create.post • clean.pre • clean.post • cli:go • cli:command-loaded • cli:pre-validate • cli:post-validate • cli:pre-execute • cli:post-execute • help:header Available hooks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 34
  35. Some remarks about hooks • you can call hooks manually,

    using cli.emit : • hooks callbacks can have a priority (default 1000): cli.on('some.event',  {        priority:  999,        post:  function()  {  /*  this  is  the  callback  */  } }); The lower the priority, the earlier the hook is executed /**  *  @param  {String|Array}  hookNames  -­‐  The  hook  name  or  an  array  of  many  hook  names  *  @param  {Object}  [data]  -­‐  The  event  payload  *  @param  {Function}  callback  -­‐  A  callback  when  the  event  has  finished  firing  */ function  emit(hookNames,  data,  callback) TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 35
  36. Some remarks about commands • do not forget to manually

    call the finished() callback: Else, the cli:post-execute hooks never gets called exports.run  =  function  (logger,  config,  cli,  finished)  {        var  i  =  0;        while  (i  <  cli.argv.quantity)  {                console.log('ponies  are  flying!');                i++;        }        if  (typeof  finished  ==  'function')  {                finished()        } };  ​  ​  ​ 9 11 TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 36
  37. package cli plugins • cli plugins are made of hooks

    and commands, and can be installed globally or locally • global install: • create the plugin somewhere, eg. /path/to/plugin • $ ti config paths.plugins -a /path/to/plugin • the plugin can contain commands and/or hooks • it will work in every Titanium project TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 37
  38. package cli plugins • local install: • put the plugin

    in the plugins folder of your project • reference it in tiapp.xml : < app   ti="http://ti.appcelerator.org">        <plugins>                <plugin  version="1.0">ticonf.playSound</plugin>        </plugins> </ app> • may only contain hooks, not commands • usable hooks: the ones after cli:post-validate ti: xmlns: ti: TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 38
  39. Some interesting CLI plugins

  40. ti-i18n • a replacement for alloy's extract-i18n command • extract

    translation strings from your app $  ti  help  i18n Usage:  titanium  i18n  <subcommand> CLI  to  manage  internationalizing  your  Titanium  app. Titanium  i18n  CLI  Subcommands:      extract      extract  i18n  strings  from  the  source  code  (js  and  tss  files) • focus on your code, not on translation dictionnaries thanks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 40
  41. ti-installr-hook • push your dev builds to Installr • install

    it: $  npm  install  -­‐g  ti-­‐installr-­‐hook  -­‐-­‐unsafe-­‐perm • Set token in tiapp.xml : • build the app: $  ti  build  -­‐p  ios  -­‐T  dist-­‐adhoc  -­‐-­‐installr • and voila! <property  name="installr.api_token">ENTER_INSTALLR_API_TOKEN_HERE</property <property  name="installr.notify"  type="bool">true</property> TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 41
  42. ti-testflight-hook • push your dev builds to Testflight (ios only).

    Do not use it. Go Installr. • install it: $  npm  install  -­‐g  ti-­‐testflight-­‐hook  -­‐-­‐unsafe-­‐perm • Set token in tiapp.xml : <property  name="testflight.api_token">ENTER_API_TOKEN_HERE</property> <property  name="testflight.team_token">ENTER_TEAM_TOKEN_HERE</property> • build the app: $  ti  build  -­‐p  ios  -­‐T  dist-­‐adhoc  -­‐-­‐testflight • and voila! thanks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 42
  43. alloy hooks into ti cli the $ alloy new task

    installs this plugin: exports.installPlugin  =  function(alloyPath,  projectPath)  {   var  id  =  'ti.alloy';   //  copy  plugin   var  srcFile  =  path.join(alloyPath,'Alloy','plugin',CONST.PLUGIN_FILE);   var  destFile  =  path.join(projectPath,'plugins',id,CONST.PLUGIN_FILE);   exports.copyFileSync(srcFile,  destFile);   //  add  the  plugin  to  tiapp.xml,  if  necessary   tiapp.init(path.join(projectPath,  'tiapp.xml'));   tiapp.installPlugin({     id:  'ti.alloy',     version:  '1.0'   }); }; thanks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 43
  44. TiNy CLI Some magic for lazy developers annoyed with long

    command lines 1. install it: $  npm  install  -­‐g  tn  -­‐-­‐unsafe-­‐perm 2. Have fun: $  titanium  build  -­‐-­‐platform  ios  -­‐-­‐device-­‐family  ipad now types: $  tn  ipad TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 44
  45. TiNy CLI Stunning features: • save build receipes: now types:

    $  tn  ship-­‐my-­‐app • lots of built-in receipes (pre-saved configurations). List: $  tn  list $  tn  save  ship-­‐my-­‐app  172B24F5-­‐1337-­‐1337-­‐1337-­‐D08EB0A7EA5D  "Check  Norris  (6MBV5WT2BD)" thanks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 45
  46. Other Titanium-related CLI

  47. ticonf-2014$ ^[p $ alloy • all alloy developers know the

    alloy CLI tool • new : turns a "classic" project into "alloy" style • compile : runs the alloy compilation • generate : generates code (controllers, widgets, etc.) TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 47
  48. $ gittio TiConf 2014 - Monitor Your App: A Complete

    Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 48
  49. $ gittio • also available as a CLI tool! $

     npm  install  -­‐g  gittio • manage native modules and alloy widgets using gitt.io: $  gittio  install  com.jolicode.pageflow $  gittio  update  -­‐g $  gittio  update  -­‐t  widget • want to try a module / widget? $  gittio  demo  dk.napp.drawer  -­‐p  ios thanks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 49
  50. tiapp.xml • not a CLI tool • a library for

    parsing and manipulating the tiapp.xml file npm  install  tiapp.xml • Usage: var  tiapp  =  require('tiapp.xml').load('./tiapp.xml'); tiapp.id  =  'com.other.name'; //  or  whichever  of  tiapp.xml  properties tiapp.write(); thanks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 50
  51. $ ticons • 2 versions of TiCons: • http://ticons.fokkezb.nl/ (PHP

    implementation) • The CLI tool ✌ $  npm  install  -­‐g  ticons Note that it has a dependacy on imagemagick . • Supports: • icons and assets • splashscreen - with 9-patch and locale support • more up-to-date than Appcelerator's alternative Spork (outdated) TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 51
  52. $ ticons • icons radius for android • orientation, platform

    and project type detection • mutualization of ios and Android MDPI images (lighter repository) $  ticons  -­‐h    Usage:  ticons  command  <args>  [options]    Commands:        icons  [options]  [input]  generate  icons        splashes  [options]  [input]  generate  splash  screens  (aka  launch  images)        assets  [input]                  generate  missing  densities  for  input  asset(s) thanks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 52
  53. $ tipi (and tetanize) • tetanize : • a "npm

    package" to "titanium commonjs module" transformer $  npm  install  -­‐g  tetanize • Usage: $  cd  /path/to/underscorejs $  tetanize • tipi : • keeps track of tetanize -generated modules installed $  npm  install  -­‐g  tipi $  tipi  install  underscore thanks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 53
  54. $ tishadow • no, promises, I won't say a word

    about TiShadow. Go use it now. thanks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 54
  55. $ ti-stealth • avoid to ship applications with console.log statements

    $  npm  install  -­‐g  ti-­‐stealth • remove or restore debug statements from the CLI: $  ti-­‐stealth  enable  [-­‐-­‐levels  <levels>]  [-­‐-­‐not-­‐levels  <levels>] $  ti-­‐stealth  restore comments / uncomments console.log and Ti.Api.info|debug|error|etc. in Resources • a sample alloy.jmk file shows how to automate logs removal in production and... guess what... thanks TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions TiConf 2014 - Monitor Your App: A Complete Panel of Titanium Monitoring Solutions 55
  56. Xavier Lacot http://jolicode.com Thank you! Go Oranje!