Getting Started with Cipr

24 Jan 2012

Now Just WTF is Cipr?

Cipr (pronounced "sipper") is part package manager, part development framework for Corona SDK. Much like Python's pip, Cipr makes it easier to share packages and integrate them into Corona apps. The Goal for Cipr is that there's a generous amount of third-party, best-of-breed packages available for use in Corona apps. This will help new developers get started developing apps sooner and reduce the re-inventing of the wheel for every app.

Installing Cipr

Cipr is developed with Python and currently only supports Mac OSX. Installing is simple on Macs because it comes with Python installed. If you're familiar with Python you can install Cipr with pip or easy_install either globally or, as preferred, in a virtualenv.

For those new to Python, don't worry, you don't need to know it. You can simply install Cipr with this command on the terminal:

sudo easy_install cipr

This installs the cipr command line utility. All your interaction with Cipr is through this command. You can get a list of available commands with:

cipr -h

Your First Project

Cipr considers each app a Project. Cipr can be used on an existing project, but for this tutorial let's create an empty project:

mkdir /tmp/hellocipr
cd /tmp/hellocipr
cipr init

cipr init initializes the current directory as a Cipr project. It will copy in the default Cipr skeleton files which includes a basic build.settings, config.lua, and main.lua. These are some of the files every Corona project has. cipr init also adds cipr.lua. This is the basic development package loader for Cipr. It should not be modified as Cipr will overwrite it with the production version during build.

This project will run at this point, but won't do anything useful. Cipr isn't entirely useful on it's own, so lets install a package:

cipr install git://github.com/six8/cipr.logging.git

As you can see, most Cipr packages are git repositories starting with git://. You can install packages from a directory as well, but this is generally only needed for package developers.

See what packages are now installed:

cipr packages -l

You should see something like this:

- cipr.logging
  - directory: /tmp/hellocipr/.cipr/packages/cipr.logging
  - source: git://github.com/six8/cipr.logging.git

Now lets try out our new package. Open up main.lua in your favorite editor and make it look like this:

-- Initialize cipr
local cipr = require 'cipr'
-- Use cipr to import the logging package
local logging = cipr.import 'cipr.logging'
-- Create a logger for this file (... is automatically replaced with this file name)
local log = logging.getLogger(...)
log:setLevel(log.DEBUG)

local x = 1

log:info('Started')
log:debug('the value of `x` is %s', x)  

Now we're ready to run this with Corona:

cipr run

You should now see something like the following in your terminal:

20120124T16:46:10 [root] INFO    Started
20120124T16:46:10 [root] DEBUG   the value of `x` is 1    

As you can see, this started Corona and ran our little app which pretty much only does logging at this point. You can now either exit Corona via the UI or hit ctrl+C in the terminal.

Let's Do More

We can add other packages to our project. Furthermore, those packages can declare dependencies that will pull in other packages. We can install cipr.ui, a collection of UI helpers for Corona:

cipr install git://github.com/six8/cipr.ui.git

This will produce a lot more output since it's also installing its dependencies. cipr packages should now look more like this:

- cipr.ui
- cipr.class
- middleclass-extras
- middleclass
- cipr.logging

cipr.class uses middleclass for classes in lua. cipr.ui depends on cipr.class so it and all of its dependencies are installed.

Let's change build.settings so our app is in landscape mode:

settings = {
    orientation = {
        default = "landscapeLeft",
        supported = {
            "landscapeLeft", "landscapeRight",
        },
    },
    iphone = {
        plist = {
            CFBundleIconFile = "Icon.png",
            CFBundleIconFiles = {
                "Icon.png" ,
                "Icon@2x.png" ,
                "Icon-72.png" ,
                "Icon-Small.png" ,
                "Icon-Small@2x.png",
                "Icon-Small-50.png"
            },
            -- When set to TRUE turns off the gloss,
            -- but leaves the rounded edges.
            UIPrerenderedIcon = true,
            UIStatusBarHidden = true,
            UIInterfaceOrientation = "UIInterfaceOrientationLandscapeLeft",

            UISupportedInterfaceOrientations = {
                "UIInterfaceOrientationLandscapeLeft",
                "UIInterfaceOrientationLandscapeRight"
            },                        
        },
    }
}

Now let's modify main.lua so we can test out cipr.ui's DebugBar:

local cipr = require 'cipr'
local log = cipr.import('cipr.logging').getLogger(...)
log:setLevel(log.DEBUG)
-- Use cipr to import the ui package
local DebugBar = cipr.import 'cipr.ui.DebugBar'

log:info('Started')

display.setDefault('background', 100, 100, 100)
display.setStatusBar( display.HiddenStatusBar )

DebugBar:new()

Now cipr run the project to see the results. You should see a mostly gray screen. Slide the arrow at the top right to the left. This will drag out the DebugBar drawer that shows some useful debug info. You can use this to help with your app development. To disable it for production apps you simply remove or disable DebugBar:new().

Exploring Some Existing Packages

Since Cipr is in early development, only a few packages exist. cipr.ui is one of the most useful of the new packages. Let's take a look at some of it's features.

EffectChain

Transitions are a powerful feature of Corona. Often you need to run several transitions on a single object before finally destroying it. In this example, we'll create a mini-particle engine that will use EffectChain to power animations.

Add this to your main.lua and cipr run it:

local EffectChain = cipr.import 'cipr.ui.EffectChain'
local random = math.random

local particleGroup = display.newGroup()

-- EffectChains are re-usable
local popBubble = EffectChain():
    fadeIn{time=500}:
    transition{y=10, time=400, transition=easing.inExpo}:
    scale{scale=4, alpha=0,time=200}:
    call(display.remove)

--[[
Create a randomly sized particle and make it float and pop
]]--
local function addNewParticle()
    local x = random(20, display.contentWidth - 20)
    local y = random(100, display.contentHeight - 20)
    local particle = display.newCircle(particleGroup, x, y, random(5, 20))
    particle:setFillColor(255, 255, 255, 200)

    -- Clone the EffectChain for this particle and play it
    popBubble:on(particle):play()
end 

timer.performWithDelay(100, addNewParticle, -2)

You should now see what looks like a bubbly drink. Don't forget to try out the DebugBar to see how it performs.

ParallaxView

Almost every 2D game has a Parallax background. ParallaxView makes it easy to create these types of backgrounds.

Add this to your main.lua and cipr run it:

local ParallaxView = cipr.import 'cipr.ui.widgets.ParallaxView'

local view = display.newGroup()
local background = display.newGroup()
view:insert(background)
-- Put our particle group from earlier on top
view:insert(particleGroup)

local pview = ParallaxView(display.contentWidth, display.contentHeight)
background:insert(pview)
background.y = display.contentHeight

-- This layer moves at 1/4 the speed
local layer1 = pview:newLayer(0.25)

local bgA = display.newRect(0, 0, 400, 220)
bgA:setFillColor(120, 120, 120)

local bgB = display.newRect(0, 0, 400, 280)
bgB:setFillColor(140, 140, 140)

local bgC = display.newRect(0, 0, 400, 200)
bgC:setFillColor(160, 160, 160)

layer1:addCol(bgA)
layer1:addCol(bgB)
layer1:addCol(bgC)  

-- This layer moves at half the speed
local layer2 = pview:newLayer(0.5)

-- A layer must have at least 2 columns. The width of all 
-- the cols combined must be at least the width of the view
-- in order to loop properly
local bg1 = display.newRect(0, 0, 300, 120)
bg1:setFillColor(90, 90, 90)

local bg2 = display.newRect(0, 0, 300, 80)
bg2:setFillColor(50, 50, 50)

local bg3 = display.newRect(0, 0, 300, 100)
bg3:setFillColor(75, 75, 75)

layer2:addCol(bg1)
layer2:addCol(bg2)
layer2:addCol(bg3)


local x = 0
local function enterFrame()
    x = x + 10
    pview:scrollTo(x, 0, 1)    
end

Runtime:addEventListener('enterFrame', enterFrame)

You should see a couple layers of backgrounds scrolling by at different rates.

Explore cipr.ui for many other useful utilities.

Building

To run your app for the Xcode simulator or submit it to the App store, you'll need to build it.

To build:

cipr build

This will package up all the files for your project under your project's build directory. Unfortunately, Corona doesn't support command line parameters so you'll have to build it from the Corona UI. Select your project's dist/Payload directory as the place to output the built app. Once your project is built, you can easily package it as an IPA:

cipr packageipa

What's Next?

Cipr is currently in development. It's being used as the basis for several games actively in development. It has a ways to go before it's production ready so use at your own risk. Any contributions to get Cipr further along will be greatly appreciated. Take a look at Cipr and cipr.ui and see if it will ease your development with Corona.

Source Control

One last note. All projects should of course be using source control. Cipr needs .ciprcfg which it creates at the root of your project. This file should be added to your source control. Cipr also creates a .cipr directory in your project. This is where it maintains installed packages. This directory should generally be excluded from source control as it may have many large packages. You should also exclude the build and dist directories.

Since .cipr isn't generally checked in, this poses a problem if you are sharing code with others. To install a project's required packages, run the following in the project's directory:

cipr install

Clom: Python command line the easy way

07 Nov 2011

Now Just WTF is Clom?

Clom is a Python Command Line Object Mapper. I wrote Clom in order to reduce the amount of repetitive coding needed to write apps that routinely deal with the command line. Fabric scripts in particular involve string manipulation to write commands which can lead to error prone, unescaped, and unsafe commands. Instead of working directly with strings and having to worry about escaping arguments yourself (or completely forgetting to), Clom allows you to work with an object interface to write commands more safely. Just as SQL ORMs allow you to write re-usable and safer SQL, Clom allows you to write re-usable and safer command line code.

Using Clom to Manage VirtualBox

To introduce you to Clom and how it's useful, let's take a look at how we could use Clom to manage some VirtualBoxes.

>>> from clom import clom
>>> clom.VBoxManage.list.runningvms()
'VBoxManage list runningvms'

Working with Clom starts with the clom object. Commands are accessed by attribute or item lookup. Each command can have sub-commands also accessed by attribute or item lookup. Calling a command doesn't execute it. A command can be stringified in order to be passed to a subprocess function, Popen, or even Fabric. Clom also offers a shortcut for easily executing commands in a shell.

>>> clom.VBoxManage.list.runningvms.shell()
INFO:clom.shell:Executing command: VBoxManage list runningvms
<CommandResult return_code=0, stdout=0 bytes, stderr=0 bytes>

Shelling Out Commands

The Clom shell interface offers many convenient ways to access the return result of a shell command.

>>> clom.VBoxManage.list.vms.shell.all()
INFO:clom.shell:Executing command: VBoxManage list vms
['"Windows Base" {949ec0af-92d0-4140-8a6c-36301ca6f695}', '"Dev_1320289635" {0a4df13c-e9f8-42c1-b09b-1f4619cca34d}']


>>> clom.VBoxManage.list.vms.shell.first()
INFO:clom.shell:Executing command: VBoxManage list vms
'"Windows Base" {949ec0af-92d0-4140-8a6c-36301ca6f695}'


>>> clom.VBoxManage.list.vms.shell.last()
INFO:clom.shell:Executing command: VBoxManage list vms
'"Dev_1320289635" {0a4df13c-e9f8-42c1-b09b-1f4619cca34d}'

You can also iterate over the lines of the command's results.

>>> for line in clom.VBoxManage.list.vms.shell():
...     print line
... 
INFO:clom.shell:Executing command: VBoxManage list vms
"Windows Base" {949ec0af-92d0-4140-8a6c-36301ca6f695}
"Dev_1320289635" {0a4df13c-e9f8-42c1-b09b-1f4619cca34d}

Shell execution returns CommandResult which allows access to stdout, stderr, and the return code of the command.

Augment with Arguments

Commands aren't very interesting without arguments, so let's add some.

>>> clom.VBoxManage.showvminfo('Windows Base', machinereadable=True, details=True).shell.all()[:5]
INFO:clom.shell:Executing command: VBoxManage showvminfo --machinereadable --details 'Windows Base'
['name="Windows Base"', 'ostype="WindowsXP"', 'UUID="949ec0af-92d0-4140-8a6c-36301ca6f695"', 'CfgFile="VirtualBox/Machines/Windows Base/Windows Base.xml"', 'SnapFldr="VirtualBox/Machines/Windows Base/Snapshots"']

There's several ways to add arguments and options. When arguments or option values are used, they're properly escaped if needed.

>>> name = 'Test VM'
>>> clom.VBoxManage.createvm(name=name, register=True, ostype='Linux').shell.all()
INFO:clom.shell:Executing command: VBoxManage createvm --ostype Linux --register --name 'Test VM'
["Virtual machine 'Test VM' is created and registered.", 'UUID: e7b73b54-7a94-4aaa-9f5f-335140a1703a', "Settings file: 'VirtualBox VMs/Test VM/Test VM.vbox'"]

Options can be specified by calling a command with keyword arguments as above or by using with_opts(). Options and arguments have a catch however. Options are always listed before arguments. VBoxManage expects the VM's name as the first argument, followed by options.

>>> clom.VBoxManage.modifyvm('Test VM').with_opts('--vrde', 'on', memory=256, usb='on')
'VBoxManage modifyvm --vrde on --usb on --memory 256 'Test VM''

Unfortunately, as expected, the code above puts the VM name after the options. We can get around this by passing the VM name as the first option. with_opts() honors positional options followed by unspecified order keyword options.

>>> clom.VBoxManage.modifyvm.with_opts('Test VM', '--vrde', 'on', memory=256, usb='on')
'VBoxManage modifyvm 'Test VM' --vrde on --usb on --memory 256'

Arguments are straight forward. They're just added to the end of the command in the order you provide.

>>> clom.VBoxManage.registervm.with_args('/path/to/vm')
'VBoxManage registervm /path/to/vm'


>>> clom.VBoxManage.setproperty.with_args('machinefolder', 'default')
'VBoxManage setproperty machinefolder default'

Calling a command is simply an alias to with_opts(**kwargs).with_args(*args). The above could also be written as a function call.

>>> clom.VBoxManage.registervm('/path/to/vm')
'VBoxManage registervm /path/to/vm'


>>> clom.VBoxManage.setproperty('machinefolder', 'default')
'VBoxManage setproperty machinefolder default'

Reusing Clom Objects

Part of the power of Clom is that each command object is re-usable. This allows you to chain and combine commands or use them many times for batch commands.

>>> vbox = clom.VBoxManage
>>> vbox.list.runningvms
'VBoxManage list runningvms'

Sub-commands can be re-used as well.

>>> list = vbox.list
>>> list.runningvms
'VBoxManage list runningvms'    
>>> list.vms
'VBoxManage list vms'   
>>> list.dvds
'VBoxManage list dvds'  

You can set default options and arguments.

>>> create_linux = vbox.createvm.with_opts(ostype='Linux')
>>> create_linux(name='My Box')
'VBoxManage createvm --ostype Linux --name 'My Box''
>>> create_linux(name='Their Box')
'VBoxManage createvm --ostype Linux --name 'Their Box''

Background Noise

Some commands need to be forked and ran in the background.

>>> clom.VBoxHeadless.with_opts(startvm='Test VM').background().shell()
INFO:clom.shell:Executing command (capture off): nohup VBoxHeadless --startvm 'Test VM' &> /dev/null &
<CommandResult return_code=0, stdout=0 bytes, stderr=0 bytes>

>>> vbox.controlvm.with_opts('Test VM').with_args('poweroff').shell()
INFO:clom.shell:Executing command: VBoxManage controlvm 'Test VM' poweroff

Shortcuts

Clom has shortcuts for most POSIX operations including:

  • with_env - Run the operation with environmental variables
  • hide_output - Redirect a command's file descriptors to /dev/null
  • output_to_file - Replace a file's contents with this command's output
  • append_to_file - Append this command's output to a file
  • pipe_to - Pipe this command to another

See the API docs for details.

Fabric

Clom's original inspiration came from doing lots of automation with Fabric.

>>> from fabric.api import local
>>> vbox = clom.VBoxManage
>>> local(vbox.list.vms)
[localhost] local: VBoxManage list vms

This of course means you can use Clom with Fabric tasks, including remote ones.

>>> def vmstart(name):
>>> ... run(vbox.startvm(name))

Clom even understands how to create Fabric commands.

>>> clom.fab.vmstart('dev')
'fab vmstart:dev'

>>> clom.fab.vmstart('dev').vmstartservice('dev', 'nginx', 'flask')
'fab vmstart:dev vmstartservice:dev,nginx,flask'

That's It

I hope you've found this introduction to Clom useful. Check out Clom's Documentation for other ways to use Clom and fork Clom on Github.

Fix for gray overlay on touch with iOS and Sencha Touch

01 Oct 2011

I've been working on a Javascript based app for the iPhone. I was originally using Phonegap which basically renders an HTML5/Javascript app in a UIWebView. I was running in to an issue where when you click on about a 20 point area on any edge of the screen, a gray overlay would appear. Most answers to this problem suggest using a transparent -webkit-tap-highlight-color or setting -webkit-user-select: none. I'm using Sencha Touch which already does this to all elements by default. In order to isolate the issue, I spent hours narrowing down all the CSS, HTML, and Javascript to the bare minimum I could manage to reproduce it.

I created a basic Phonegap app with this gist. Note that the issue only shows up if you have the status bar hidden (in this case with UIStatusBarHidden set to YES). Below is a screenshot of what it looks like. On the left is the regular view. On the right is what it looks like when you touch the edge of the screen. The effect is subtle, but enough to be annoying if your app requires clicking on the edge of the screen a lot. It ends up looking like the screen is flickering.

Side by side image of iPhone

Once I was able to distill the issue down to the basic code it was much easier to track down. Apparently the issue only appears when you add any one of the mouse events – mouseup, mousedown, mousemove, or click – to the document.

This bit is the main offender:

document.addEventListener('click', function(){}, true);

Oddly enough this doesn't happen with any of the touch events – touchup, touchdown, or touchmove.

After digging around in Sencha code for a while (which seems to be the only way to understand Sencha Touch) I figured out that this is the offending code:

Ext.gesture.Manager = new Ext.AbstractManager({

    // ...
    
    defaultPreventedMouseEvents: ['click'],

    // ...

    attachListeners: function() {
        Ext.iterate(this.eventNames, function(key, name) {
            document.addEventListener(name, this.listenerWrappers[key], false);
        }, this);

        if (Ext.supports.Touch) {
            // Right here the click event gets attached on a touch device.
            this.defaultPreventedMouseEvents.forEach(function(name) {
                document.addEventListener(name, this.listenerWrappers['mouse'], true);
            }, this);
        }
    },
    
    // ...
});

The click event ends up being attached to document when it's a touch device. I'm not sure what case prompted Sencha to do this, but I can't see any reason for it being here especially since it's the cause of this issue.

So the fix is simple. Instead of modifying Sencha's code, I just put this at the top of my Javascript after loading Sencha's Javascript:

// Prevent sencha from attaching to the 'click' event which
// will make a gray overlay appear when you touch the each of the screen.
Ext.gesture.Manager.defaultPreventedMouseEvents = [];

Note that these issues were reproduced with Sencha Touch 1.1.0 and iOS 4.3 but may not be limited to these versions.

I tried googling other solutions of course, but could only find other reports such as Flicker when tapping edge of viewport.