run: add option to do buildservices on sandbox-services

--- config/main.dev.coffee
+++ config/main.dev.coffee
@@ -510,20 +510,21 @@

         echo "Usage: "
         echo ""
-        echo "  run                    : to start koding"
-        echo "  run killall            : to kill every process started by run script"
-        echo "  run install            : to compile/install client and "
-        echo "  run buildclient        : to see of specified worker logs only"
-        echo "  run logs               : to see all workers logs"
-        echo "  run log [worker]       : to see of specified worker logs only"
-        echo "  run buildservices      : to initialize and start services"
-        echo "  run services           : to stop and restart services"
-        echo "  run worker             : to list workers"
-        echo "  run chaosmonkey        : to restart every service randomly to test resilience."
-        echo "  run testendpoints      : to test every URL endpoint programmatically."
-        echo "  run printconfig        : to print koding config environment variables (output in json via --json flag)"
-        echo "  run worker [worker]    : to run a single worker"
-        echo "  run help               : to show this list"
+        echo "  run                       : to start koding"
+        echo "  run killall               : to kill every process started by run script"
+        echo "  run install               : to compile/install client and "
+        echo "  run buildclient           : to see of specified worker logs only"
+        echo "  run logs                  : to see all workers logs"
+        echo "  run log [worker]          : to see of specified worker logs only"
+        echo "  run buildservices         : to initialize and start services"
+        echo "  run buildservices sandbox : to initialize and start services on sandbox"
+        echo "  run services              : to stop and restart services"
+        echo "  run worker                : to list workers"
+        echo "  run chaosmonkey           : to restart every service randomly to test resilience."
+        echo "  run testendpoints         : to test every URL endpoint programmatically."
+        echo "  run printconfig           : to print koding config environment variables (output in json via --json flag)"
+        echo "  run worker [worker]       : to run a single worker"
+        echo "  run help                  : to show this list"
         echo ""

       }
@@ -665,6 +666,17 @@
         go run ./go/src/socialapi/workers/migrator/main.go -c #{socialapi.configFilePath}
       }

+      function sandbox_buildservices () {
+        SANDBOX_SERVICES=54.165.122.100
+        SANDBOX_WEB_1=54.165.177.88
+        SANDBOX_WEB_2=54.84.179.170
+
+        echo "cd /opt/koding; ./run buildservices" | ssh root@$SANDBOX_SERVICES @/bin/bash
+
+        echo "sudo supervisorctl restart all"      | ssh ec2-user@$SANDBOX_WEB_1 /bin/bash
+        echo "sudo supervisorctl restart all"      | ssh ec2-user@$SANDBOX_WEB_2 /bin/bash
+      }
+
       if [[ "$1" == "killall" ]]; then

         kill_all
@@ -698,6 +710,18 @@
         services

       elif [ "$1" == "buildservices" ]; then
+
+        if [ "$2" == "sandbox" ]; then
+          read -p "This will destroy sandbox databases (y/N)" -n 1 -r
+          echo ""
+          if [[ ! $REPLY =~ ^[Yy]$ ]]
+          then
+              exit 1
+          fi
+
+          sandbox_buildservices
+          exit 0
+        fi

         check_service_dependencies


Fix specs

Fix specs

--- spec/autocomplete-manager-integration-spec.coffee
+++ spec/autocomplete-manager-integration-spec.coffee
@@ -105,7 +105,7 @@

           # Check suggestions
           suggestions = ['function', 'if', 'left', 'shift']
-          [].forEach.call editorView.querySelectorAll('.autocomplete-plus li span'), (item, index) ->
+          [].forEach.call editorView.querySelectorAll('.autocomplete-plus li span.word'), (item, index) ->
             expect(item.innerText).toEqual(suggestions[index])

       it 'should not show the suggestion list when no suggestions are found', ->


fix grunt version at 0.4.5 because 1.0 breaks several contrib packages dependencies

--- bin/init.coffee
+++ bin/init.coffee
@@ -9,20 +9,17 @@

 NPM_DEV_PACKAGES = [
   "bumble-docs", "bumble-test"
-  "grunt","grunt-available-tasks","grunt-coffee-react","grunt-contrib-clean","grunt-contrib-coffee",
+  "grunt@0.4.5","grunt-available-tasks","grunt-coffee-react","grunt-contrib-clean","grunt-contrib-coffee",
   "grunt-contrib-cssmin","grunt-contrib-watch","grunt-newer","grunt-react","grunt-scp","grunt-shell",
   "grunt-sync", "grunt-webpack"
   "babel-core", "babel-loader", "babel-preset-react", "babel-preset-es2015", "coffee-loader", "cjsx-loader"
 ]
-
-unless Fs.existsSync('./Gruntfile.js') || Fs.existsSync('./Gruntfile.coffee')
+
+unless Fs.existsSync('./Gruntfile.js') || Fs.existsSync('./Gruntfile.coffee')
   console.log 'creating: ./Gruntfile.coffee'
   Fs.copySync('node_modules/bumble-build/bin/lib/Gruntfile.coffee', './Gruntfile.coffee')
-
+
 BUtil.installNodePackage(npmPackage, {addFlags: "--save-dev"}) for npmPackage in NPM_DEV_PACKAGES
 BUtil.systemCmd("grunt")

 console.log "\n\nAll done.  You may need to install grunt-cli globally like `sudo npm install -g grunt-cli` \n\n\n"
-
-
-


Do AMD detection before CommonJS to work around conflict with AngularMocks

Since AngularMocks defines a global variable named "module", it will
force Bacon into thinking it's running inside a CommonJS context, not
AMD

--- src/Bacon.coffee
+++ src/Bacon.coffee
@@ -1470,10 +1470,10 @@
   clearInterval: (id) -> clearInterval(id)
   now: -> new Date().getTime()

-if module?
+if define? and define.amd?
+  define [], -> Bacon
+else if module?
   module.exports = Bacon # for Bacon = require 'baconjs'
   Bacon.Bacon = Bacon # for {Bacon} = require 'baconjs'
 else
-  if define? and define.amd?
-    define [], -> Bacon
   @Bacon = Bacon # otherwise for execution context


:fire: selectionIndex instance var

:fire: selectionIndex instance var

--- lib/move-selected-text.coffee
+++ lib/move-selected-text.coffee
@@ -6,15 +6,15 @@
   packPath = atom.packages.resolvePackagePath(pack)
   require "#{packPath}/lib/#{path}"

+{pointIsAtEndOfLine, getVimLastBufferRow} = requireFrom('vim-mode-plus', 'utils')
+swrap = requireFrom('vim-mode-plus', 'selection-wrapper')
 Base = requireFrom('vim-mode-plus', 'base')
 TransformString = Base.getClass('Operator')
-{pointIsAtEndOfLine, getVimLastBufferRow} = requireFrom('vim-mode-plus', 'utils')
-swrap = requireFrom('vim-mode-plus', 'selection-wrapper')

 newState = ->
   selectedTexts: null
   checkpoint: null
-  overwritten: null
+  overwrittenBySelection: null

 stateByEditor = new Map
 disposableByEditor = new Map
@@ -32,8 +32,9 @@
           stateByEditor.delete(@editor)

       disposableByEditor.set @editor, @editor.onDidDestroy =>
+        disposable.dispose()
         stateByEditor.delete(@editor)
-        disposable.dispose()
+        disposableByEditor.delete(@editor)

   getSelectedTexts: ->
     @editor.getSelections().map((selection) -> selection.getText()).join("\n")
@@ -49,21 +50,22 @@
       @editor.transact =>
         @countTimes =>
           return if (not @isLinewise()) and (not @isMovable(topSelection))
-
-          for selection, i in selections
-            @selectionIndex = i
-            @selectionIndex = selections.length - (i + 1) if @direction is 'down'
+          for selection in selections
             @extendMovingArea(selection)
             @mutate(selection) if @isMovable(selection)

-  getInitialOverwrittenArea: ->
+  getOverwrittenBySelection: ->
+    overwrittenBySelection = new Map
     selections = @editor.getSelections()
     if @isLinewise()
-      selections.map (selection) ->
-        swrap(selection).getRows().map(-> '').join("\n")
-    else
-      selections.map (selection) ->
-        _.multiplyString(' ', selection.getBufferRange().getExtent().column)
+      selections.forEach (selection) ->
+        overwrittenBySelection.set selection,
+          swrap(selection).getRows().map(-> '').join("\n")
+    else
+      selections.forEach (selection) ->
+        overwrittenBySelection.set selection,
+          _.multiplyString(' ', selection.getBufferRange().getExtent().column)
+    overwrittenBySelection

   withUndoJoin: (fn) ->
     state = stateByEditor.get(@editor)
@@ -72,8 +74,8 @@
       state.checkpoint = @editor.createCheckpoint()
       state.overwritten = null

-    unless state.overwritten?
-      state.overwritten = @getInitialOverwrittenArea()
+    unless state.overwrittenBySelection?
+      state.overwrittenBySelection = @getOverwrittenBySelection()

     fn()

@@ -81,10 +83,11 @@
     if isSequential
       @editor.groupChangesSinceCheckpoint(state.checkpoint)

-  getOverwrittenText: (replacedText) ->
-    state = stateByEditor.get(@editor)
-    overwrittenArea = state.overwritten[@selectionIndex]
-    replacedText = switch @direction
+  getOverwrittenForSelection: (selection, replacedText) ->
+    {overwrittenBySelection} = stateByEditor.get(@editor)
+    overwrittenArea = overwrittenBySelection.get(selection)
+
+    overwritten = switch @direction
       when 'up'
         [overwritten, rest...] = overwrittenArea.split("\n")
         overwrittenArea = [rest..., replacedText].join("\n")
@@ -102,8 +105,8 @@
         overwrittenArea = [replacedText, rest...].join("")
         overwritten

-    state.overwritten[@selectionIndex] = overwrittenArea
-    replacedText
+    overwrittenBySelection.set(selection, overwrittenArea)
+    overwritten

   isLinewise: ->
     switch @vimState.submode
@@ -144,22 +147,23 @@
   moveCharacterwise: (selection) ->
     reversed = selection.isReversed()

+    # Pre mutate
     translation = switch @direction
       when 'up' then [[-1, 0], [-1, 0]]
       when 'down' then [[1, 0], [1, 0]]

     fromRange = selection.getBufferRange()
     toRange = fromRange.translate(translation...)
-    @swapTextInRange(fromRange, toRange)
-    selection.setBufferRange(toRange, {reversed})
-
-  swapTextInRange: (fromRange, toRange) ->
+
+    # Swap text from fromRange to toRange
     @complementSpacesToPoint(toRange.end)
     movingText = @editor.getTextInBufferRange(fromRange)
     replacedText = @editor.getTextInBufferRange(toRange)
-    replacedText = @getOverwrittenText(replacedText) if @isOverwrite()
+    replacedText = @getOverwrittenForSelection(selection, replacedText) if @isOverwrite()
     @editor.setTextInBufferRange(fromRange, replacedText)
     @editor.setTextInBufferRange(toRange, movingText)
+
+    selection.setBufferRange(toRange, {reversed})

   complementSpacesToPoint: ({row, column}) ->
     eol = @editor.bufferRangeForBufferRow(row).end
@@ -178,27 +182,26 @@
       when 'down' then [[0, 0], [1, 0]]

     swrap(selection).translate(translation...)
-
-    lineTexts = swrap(selection).lineTextForBufferRows()
-    @rotateRows(lineTexts)
-    newText = lineTexts.join("\n") + "\n"
-    range = selection.insertText(newText)
+    range = selection.insertText(@getNewText(selection))

     # Post mutate
     range = range.translate(translation.reverse()...)
     selection.setBufferRange(range, {reversed})
     @editor.scrollToCursorPosition({center: true})

-  rotateRows: (lineTexts) ->
+  getNewText: (selection) ->
+    # Rotate row
+    lineTexts = swrap(selection).lineTextForBufferRows()
     switch @direction
       when 'up'
         replacedText = lineTexts.shift()
-        replacedText = @getOverwrittenText(replacedText) if @isOverwrite()
+        replacedText = @getOverwrittenForSelection(selection, replacedText) if @isOverwrite()
         lineTexts.push(replacedText)
       when 'down'
         replacedText = lineTexts.pop()
-        replacedText = @getOverwrittenText(replacedText) if @isOverwrite()
+        replacedText = @getOverwrittenForSelection(selection, replacedText) if @isOverwrite()
         lineTexts.unshift(replacedText)
+    lineTexts.join("\n") + "\n"

 class MoveSelectedTextDown extends MoveSelectedTextUp
   @extend()
@@ -237,25 +240,30 @@
   moveCharacterwise: (selection) ->
     reversed = selection.isReversed()

+    # Pre mutate
     translation = switch @direction
       when 'right' then [[0, 0], [0, +1]]
       when 'left' then [[0, -1], [0, 0]]

     swrap(selection).translate(translation...)
-    newText = @rotate(selection.getText())
-    range = selection.insertText(newText)
+    range = selection.insertText(@getNewText(selection))
+
+    # Post mutate
     range = range.translate(translation.reverse()...)
     selection.setBufferRange(range, {reversed})
-
-  rotate: (text) ->
+    @editor.scrollToCursorPosition({center: true})
+
+  getNewText: (selection) ->
+    # Rotate column
+    text = selection.getText()
     switch @direction
       when 'right'
         [moving..., replacedText] = text
-        replacedText = @getOverwrittenText(replacedText) if @isOverwrite()
+        replacedText = @getOverwrittenForSelection(selection, replacedText) if @isOverwrite()
         replacedText + moving.join('')
       when 'left'
         [replacedText, moving...] = text
-        replacedText = @getOverwrittenText(replacedText) if @isOverwrite()
+        replacedText = @getOverwrittenForSelection(selection, replacedText) if @isOverwrite()
         moving.join('') + replacedText

 class MoveSelectedTextLeft extends MoveSelectedTextRight


fix namespace

fix namespace

--- src/binding.coffee
+++ src/binding.coffee
@@ -16,9 +16,9 @@
     else
         name = attrName

-    j = name.indexOf '-'
+    j = name.indexOf ':'
     if j < 0
-        j = name.indexOf ':'
+        j = name.indexOf '-'
     if j < 0
         return { noNs: true }

@@ -109,7 +109,7 @@
         fn: ->
             if @.directive.init
                 if alight.debug.directive
-                    if @.directive.scope or @.directive.ChangeDetector
+                    if @.directive.scope
                         console.warn "#{@.ns}-#{@.name} uses scope and init together, probably you need use link instead of init"
                 @.env.changeDetector = @.cd
                 @.cd.scope.$changeDetector = @.cd


:art: Tidy up status-bar manager

:art: Tidy up status-bar manager

Move 'inline-block' class to a parent element so that we don't
have to worry about adding/removing specific classes from the
main element; we can just replace the className wholesale.

--- lib/status-bar-manager.coffee
+++ lib/status-bar-manager.coffee
@@ -1,7 +1,5 @@
 {Disposable, CompositeDisposable} = require 'event-kit'

-# Mode names are combined with submode names with the `.` character.
-# If a mode has submodes, they need to be enumerated here.
 ContentsByMode =
   'insert':               ["status-bar-vim-mode-insert",  "Insert"]
   'insert.replace':       ["status-bar-vim-mode-insert",  "Replace"]
@@ -11,36 +9,29 @@
   'visual.linewise':      ["status-bar-vim-mode-visual",  "Visual Line"]
   'visual.blockwise':     ["status-bar-vim-mode-visual",  "Visual Block"]

-ClassesByMode = []
-
-for mode, [klass, html] of ContentsByMode
-  ClassesByMode.push(klass) unless klass in ClassesByMode
-
 module.exports =
 class StatusBarManager
   constructor: ->
     @element = document.createElement("div")
     @element.id = "status-bar-vim-mode"
-    @element.classList.add("inline-block")
+
+    @container = document.createElement("div")
+    @container.className = "inline-block"
+    @container.appendChild(@element)

   initialize: (@statusBar) ->

   update: (currentMode, currentSubmode) ->
     currentMode = currentMode + "." + currentSubmode if currentSubmode?
-    return unless currentMode of ContentsByMode
-
-    # remove all the classes from @element before adding the right class
-    for klass in ClassesByMode
-      @element.classList.remove(klass)
-
-    [klass, html] = ContentsByMode[currentMode]
-    @element.classList.add(klass)
-    @element.innerHTML = html
+    if newContents = ContentsByMode[currentMode]
+      [klass, text] = newContents
+      @element.className = klass
+      @element.textContent = text

   # Private

   attach: ->
-    @tile = @statusBar.addRightTile(item: @element, priority: 20)
+    @tile = @statusBar.addRightTile(item: @container, priority: 20)

   detach: ->
     @tile.destroy()


Switching to centered positions and drawing

--- src/bounded.coffee
+++ src/bounded.coffee
@@ -22,7 +22,8 @@
     collisionMargin: Point(0, 0)

   ###*
-  The position of this game object, the top left point in the local transform.
+  The position of this game object. By default it is the top left point.
+  Redefining the center method will change the relative position.

   @returns The position of this object
   @type Point
@@ -56,7 +57,7 @@

   ###*
   The bounds method returns infomation about the location
-  of the object and its dimensions with optional offsets
+  of the object and its dimensions with optional offsets.

   @name bounds
   @methodOf Bounded#
@@ -65,27 +66,31 @@
   @param {number} yOffset the amount to shift the y position
   ###
   bounds: (xOffset, yOffset) ->
-    x: I.x - I.width/2 + (xOffset || 0)
-    y: I.y - I.height/2 + (yOffset || 0)
+    center = self.center()
+
+    x: center.x - I.width/2 + (xOffset || 0)
+    y: center.y - I.height/2 + (yOffset || 0)
     width: I.width
     height: I.height

   ###*
   The centeredBounds method returns infomation about the center
-  of the object along with the midpoint of the width and height
+  of the object along with the midpoint of the width and height.

   @name centeredBounds
   @methodOf Bounded#
   ###
   centeredBounds: () ->
-    x: I.x
-    y: I.y
+    center = self.center()
+
+    x: center.x
+    y: center.y
     xw: I.width/2
     yw: I.height/2

   ###*
   The center method returns the {@link Point} that is
-  the center of the object
+  the center of the object.

   @name center
   @methodOf Bounded#


activity: s/_isCreating/_isRouting/g

activity: s/_isCreating/_isRouting/g

--- client/activity/lib/components/createprivatechannelmodal/index.coffee
+++ client/activity/lib/components/createprivatechannelmodal/index.coffee
@@ -96,7 +96,7 @@
   onClose: ->

     return  unless @state.selectedThread
-    return  if @_isCreating
+    return  if @_isRouting

     channel = @state.selectedThread.get('channel').toJS()

@@ -139,13 +139,13 @@

     { createPrivateChannel } = CreateChannelFlux.actions.channel

-    @_isCreating = yes
+    @_isRouting = yes

     createPrivateChannel(options)
       .then ({channel}) ->
         kd.singletons.router.handleRoute "/Messages/#{channel.id}"
       .catch =>
-        @_isCreating = no
+        @_isRouting = no

   onChange: (event) ->


Decimals and Duration parameters now part of the options object and available using data attributes, also added new option autostart which will allow you to start without calling .start()

--- src/counter.coffee
+++ src/counter.coffee
@@ -11,12 +11,15 @@
     @VERSION: '1.0.0'

     @DEFAULTS:
-      useEasing: yes
-      useGrouping: yes
+      autostart: no
+      easing: yes
+      grouping: yes
       separator: ','
       decimal: '.'
       prefix: ''
       suffix: ''
+      decimals: 0
+      duration: 2

     ###*
      * Counter
@@ -25,31 +28,31 @@
      * @param {number} target element selector or var of previously selected html element where counting occurs
      * @param {number} startVal The value you want to begin at
      * @param {number} endVal The value you want to arrive at
-     * @param {number} [decimals=0] Tumber of decimal places
-     * @param {number} [duration=2] Duration of animation in second
      * @param {object} [options] Optional object of options (see below)
      *
     ###
-    constructor: (target, startVal, endVal, decimals, duration, options = {}) ->
+    constructor: (target, startVal, endVal, options = {}) ->
       @polyFill()

-      @root      = {target, startVal, endVal, decimals, duration}
       @element   = if typeof target is 'string' then document.querySelector target else target
-      @startVal  = +startVal
-      @endVal    = +endVal
-      @countDown = if startVal > endVal then yes else no
+      @options   = @extend options, @getAttributes(), Counter.DEFAULTS
+      @startVal  = @_startVal = +startVal
+      @endVal    = @_endVal = +endVal
+      @countDown = if @startVal > @endVal then yes else no
       @startTime = null
       @timestamp = null
       @remaining = null
       @rAF       = null
       @frameVal  = @startVal
-      @decimals  = Math.max 0, decimals or 0
+      @decimals  = Math.max 0, @options.decimals
       @dec       = 10 ** @decimals
-      @duration  = duration * 1000 or 2000
-      @options   = @extend options, @getAttributes(@element), Counter.DEFAULTS
-      @options.useGrouping = no if @options.separator is ''
-
-      @printValue @startVal
+      @duration  = @options.duration * 1000
+      @options.grouping = no if @options.separator is ''
+
+      if @options.autostart
+        @start()
+      else
+        @printValue @startVal

     ###*
      * Make sure requestAnimationFrame and cancelAnimationFrame are defined
@@ -106,21 +109,35 @@
       obj

     ###*
+     * Get element data attribute
+     *
+     * @method getDataAttribute
+     * @param {object} element
+     * @param {string} attribute
+     *
+    ###
+    getDataAttribute: (element = @element, attribute) ->
+      element.getAttribute "data-#{attribute}"
+
+    ###*
      * Get options from `data-*` attributes
      *
      * @method getAttributes
-     * @param (object) element
-     *
-    ###
-    getAttributes: (element) ->
+     * @param {object} element
+     *
+    ###
+    getAttributes: (element = @element) ->
       return element.dataset if element.dataset

-      useEasing: element.getAttribute 'data-easing'
-      useGrouping: element.getAttribute 'data-grouping'
-      separator: element.getAttribute 'data-separator'
-      decimal: element.getAttribute 'data-decimal'
-      prefix: element.getAttribute 'data-prefix'
-      suffix: element.getAttribute 'data-suffix'
+      autostart: @getDataAttribute 'autostart'
+      easing:    @getDataAttribute 'easing'
+      grouping:  @getDataAttribute 'grouping'
+      separator: @getDataAttribute 'separator'
+      decimal:   @getDataAttribute 'decimal'
+      decimals:  @getDataAttribute 'decimals'
+      duration:  @getDataAttribute 'duration'
+      prefix:    @getDataAttribute 'prefix'
+      suffix:    @getDataAttribute 'suffix'

     ###*
      * Print value to the target element
@@ -167,13 +184,13 @@
       @remaining = @duration - progress

       # To ease or not to ease
-      if @options.useEasing
+      if @options.easing
         if @countDown
           @frameVal = @startVal - @easeOutExpo progress, 0, (@startVal - @endVal), @duration
         else #@countDown
           @frameVal = @easeOutExpo progress, @startVal, @endVal - @startVal, @duration

-      else #@options.useEasing
+      else #@options.easing
         if @countDown
           @frameVal = @startVal - (@startVal - @endVal) * (progress / @duration)
         else #@countDown
@@ -211,7 +228,7 @@
       if not isNaN(@endVal) and not isNaN @startVal
         @rAF = requestAnimationFrame @count
       else #isNaN @endVal and isNaN @startVal
-        console.error 'countUp error: startVal or endVal is not a number'
+        console.error 'Counter error: startVal or endVal is not a number'
         @printValue()

       @
@@ -235,7 +252,7 @@
     ###
     reset: ->
       @startTime = null
-      @startVal  = @root.startVal
+      @startVal  = @_startVal

       cancelAnimationFrame @rAF
       @printValue @startVal
@@ -273,7 +290,7 @@
       x2  = if x.length > 1 then @options.decimal + x[1] else ''
       rgx = /(\d+)(\d{3})/

-      if @options.useGrouping
+      if @options.grouping
         while rgx.test x1
           x1 = x1.replace rgx, "$1#{@options.separator}$2"


Update scripts path in the creator

Update scripts path in the creator

--- src/creator.coffee
+++ src/creator.coffee
@@ -9,7 +9,7 @@
   constructor: (path) ->
     @path = path
     @templateDir = "#{__dirname}/templates"
-    @scriptsDir  = "#{__dirname}/hubot/scripts"
+    @scriptsDir  = "#{__dirname}/scripts"

   # Create a folder if it doesn't already exist
   #


bump reattach timeout to 2 minutes

bump reattach timeout to 2 minutes

--- src/decking.coffee
+++ src/decking.coffee
@@ -15,6 +15,7 @@
 docker = new Docker socketPath: "/var/run/docker.sock"

 log = Logger.log
+logStream = (name, data) -> log "#{Table.padName(name, "(", ")")} #{data}"

 version = require("#{__dirname}/../package.json").version

@@ -100,9 +101,12 @@
       isRunning container, (err, running) ->
         if not running
           Table.render name, "starting..."
-          container.start callback
+          container.start (err) ->
+            return callback(err) if err
+            Table.renderOk name
+            callback null
         else
-          Table.render name, "skipping (already running)"
+          Table.render name, "already running"
           callback null

     resolveContainers @config, cluster, (list) ->
@@ -124,9 +128,12 @@
       isRunning container, (err, running) ->
         if running
           Table.render name, "stopping..."
-          container.stop t:5, callback
+          container.stop t:5, (err) ->
+            return callback(err) if err
+            Table.renderOk name
+            callback null
         else
-          Table.render name, "skipping (already stopped)"
+          Table.render name, "already stopped"
           callback null

     resolveContainers @config, cluster, (list) ->
@@ -144,10 +151,17 @@
         if running
           Table.render name, "restarting..."
           container.stop t:5, (err) ->
-            container.start callback
+            return callback(err) if err
+            container.start (err) ->
+              return callback(err) if err
+              Table.renderOk name
+              callback null
         else
           Table.render name, "starting..."
-          container.start callback
+          container.start (err) ->
+            return callback(err) if err
+            Table.renderOk name
+            callback null

     resolveContainers @config, cluster, (list) ->

@@ -165,12 +179,12 @@
         isRunning container, (err, running) ->
           if running
             attach name, container, false, ->
-              Table.render name, "re-attached"
+              logStream name, "re-attached"
           else
-            if attempts < 100
+            if attempts < 200
               reAttach name, container, attempts + 1
             else
-              Table.render name, "max re-attach attempts reached, bailing..."
+              logStream name, "max re-attach attempts reached, bailing..."
       , timeout

     attach = (name, container, fetchLogs, callback) ->
@@ -185,7 +199,7 @@
         new MultiplexStream container, stream, Table.padName(name, "(", ")")

         stream.on "end", ->
-          Table.render name, "gone away, will try to re-attach for one minute..."
+          logStream name, "gone away, will try to re-attach for two minutes..."
           reAttach name, container

         callback? err


Making it a bit more CoffeeScript-like :)

--- src/detectr.coffee
+++ src/detectr.coffee
@@ -132,7 +132,7 @@
   ###
   detectr = (config, options) ->
     # Make sure the config object exists and it has some tests in it
-    return undefined unless (config and config.tests)
+    return undefined unless config and config.tests

     globalOptions = options

@@ -169,7 +169,7 @@

     doOrientationChange = (condition) ->
       if condition()
-        return false if detectr.Display.orientation == detectResultCache['landscape']
+        return false if detectr.Display.orientation is detectResultCache['landscape']

         newResult = detectr.defaultTests.tests['landscape'].result

@@ -186,14 +186,14 @@
         detectResultCache['portrait'] = undefined

       else
-        return false if detectr.Display.orientation == detectResultCache['portrait']
+        return false if detectr.Display.orientation is detectResultCache['portrait']

         newResult = detectr.defaultTests.tests['portrait'].result

         htmlClassName = document.documentElement.className

         if contains htmlClassName, detectResultCache['landscape']
-          htmlClassName = htmlClassName.replace(detectResultCache['landscape'], newResult)
+          htmlClassName = htmlClassName.replace detectResultCache['landscape'], newResult
           document.documentElement.className = htmlClassName

         detectCache['landscape'] = true
@@ -207,7 +207,7 @@

     window.addEventListener 'resize', (-> doOrientationChange(-> detectr.Display.pageWidth() >= detectr.Display.pageHeight())), false

-    window.addEventListener 'orientationchange', (-> doOrientationChange(-> Math.abs(window.orientation) == 90)), false
+    window.addEventListener 'orientationchange', (-> doOrientationChange(-> Math.abs(window.orientation) is 90)), false

     detectr.Display.orientation = detectResultCache['landscape'] or detectResultCache['portrait']


Compact packages array

Compact packages array

--- src/fetcher.coffee
+++ src/fetcher.coffee
@@ -39,7 +39,7 @@
                   bestMatch = metadata
               bestMatch

-            callback(null, packages)
+            callback(null, _.compact(packages))

   run: (options) ->
     @getAvailablePackages options.argv.atomVersion, (error, packages) ->


Fixed XHR error response on exception

Fixed XHR error response on exception

--- src/generic.coffee
+++ src/generic.coffee
@@ -199,6 +199,7 @@
                 # But this way the promise will be rejected as expected
                 #
                 console.error( "[XHR] Error during request", xhrError )
+                @createErrorResponse( xhrError )

             return @deferred.promise

@@ -334,12 +335,13 @@
                 statusText: @transport.statusText
             )

-        createErrorResponse: () ->
+        createErrorResponse: ( xhrException ) ->
             @deferred.reject(
                 request:    @request
                 response:   @transport.responseText || @transport.statusText
                 status:     @transport.status
                 statusText: @transport.statusText
+                exception:  xhrException
             )

         createTimeoutResponse: () ->


Test that --import-helpers actually imports helper functions

All of this work is for nothing if the compiled output doesn't change!

--- test/compilation.coffee
+++ test/compilation.coffee
@@ -17,6 +17,12 @@
 test "--bare", ->
   eq -1, CoffeeScript.compile('x = y', bare: on).indexOf 'function'
   ok 'passed' is CoffeeScript.eval '"passed"', bare: on, filename: 'test'
+
+test "--import-helpers", ->
+  # in bare mode with "--import-helpers", the "modulo" helper should be a reference,
+  # therefore no 'function' keywords should exist in the output
+  eq -1, CoffeeScript.compile('5 %% 3', importHelpers: on, bare: on).indexOf 'function'
+  ok 2 is CoffeeScript.eval '5 %% 3', importHelpers: on, bare: on, filename: 'test'

 test "header (#1778)", ->
   header = "// Generated by CoffeeScript #{CoffeeScript.VERSION}\n"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/core_object.coffee
+++ test/core_object.coffee
@@ -44,6 +44,8 @@
   equals o.test(), "my_val"
   equals o.test2, "cool"

+test ""
+
 test "#include multiple", ->
   o = Core
     test: "my_val"


add code to detect TeX utter misbehavior where it gets stuck on PDF file not being wrtiable (for totally incomprehensible reasons)


Fixed sorting.

Fixed sorting.

--- src/js/main.coffee
+++ src/js/main.coffee
@@ -143,6 +143,7 @@
     if @paletteType is 'PICO8'
       @sortBy = @sortBy.join(',').replace('luma', 'l').split(',')
     @createData()
+    @sortBy.unshift 'diffLuma'
     if @sortBy.length > 0
       @mixed.sort SortUtils.dynamicMultiSort @sortBy
     @createTable()


filter oldDNS response to relevant data

filter oldDNS response to relevant data

--- src/lib/dns.coffee
+++ src/lib/dns.coffee
@@ -61,7 +61,7 @@
                         # letter of the fourth, and the full parts of the last three
                         # This isn't perfect, especially because of:
                         # https://publicsuffix.org/list/effective_tld_names.dat
-                        #
+                        #
                         # See: https://github.com/okTurtles/dnschain/issues/107 !
                         domain = [domain[-4..1][0][-1..]].concat(domain[-3..]).join '.'
                     else
@@ -166,8 +166,7 @@
             else
                 @log.debug gLineInfo("deferring request"), {q:q}
                 @dnschain.cache.resolveOldDNS req, (code, packet) =>
-                    _.assign res, _.pick packet, ['edns_version', 'edns_options',
-                        'edns', 'answer', 'authority', 'additional']
+                    _.assign res, packet
                     if code
                         @sendErr res, code, cb
                     else
@@ -178,6 +177,8 @@
             res = new Packet()
             sig = "oldDNS{#{@method}}"
             q = req.question[0]
+            filterRes = (p) ->
+                _.pick p, ['edns_version', 'edns_options', 'edns', 'answer', 'authority', 'additional']

             @log.debug {fn:sig+':start', q:q}

@@ -211,20 +212,20 @@
                 req2.on 'end', =>
                     if success
                         @log.debug gLineInfo('success!'), {q:q, res: _.omit(res, '_socket')}
-                        cb null, res
+                        cb null, filterRes(res)
                     else
                         # TODO: this is noisy.
                         #       also make log output look good in journalctl
                         # you can log IP with: res._socket.remote.address
                         @log.warn gLineInfo('oldDNS lookup failed'), {q:q, err:req2.DNSErr}
-                        cb NAME_RCODE.SERVFAIL, res
+                        cb NAME_RCODE.SERVFAIL, filterRes(res)
                 # @log.debug {fn:"beforesend", req:req2}
                 req2.send()
             else if @method is gConsts.oldDNS.NODE_DNS
                 dns.resolve q.name, QTYPE_NAME[q.type], (err, addrs) =>
                     if err
                         @log.debug {fn:sig+':fail', q:q, err:err?.message}
-                        cb NAME_RCODE.SERVFAIL, res
+                        cb NAME_RCODE.SERVFAIL, filterRes(res)
                     else
                         # USING THIS METHOD IS DISCOURAGED BECAUSE IT DOESN'T
                         # PROVIDE US WITH CORRECT TTL VALUES!!
@@ -232,7 +233,7 @@
                         ttl = Math.floor(Math.random() * 3600) + 30
                         res.answer.push (addrs.map gIP2type(q.name, ttl, QTYPE_NAME[q.type]))...
                         @log.debug {fn:sig+':success', answer:res.answer, q:q.name}
-                        cb null, res
+                        cb null, filterRes(res)
             else
                 # refuse all such queries
                 cb NAME_RCODE.REFUSED, res


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -3,7 +3,8 @@
 meter ui to track arbitrary numeric attributes.

-player = GameO
+player = GameObject
+

 player.position()


fixing bug where rs wasn't passed as first arg

--- src/natives.coffee
+++ src/natives.coffee
@@ -447,8 +447,9 @@
             return 0 unless stats?
             if stats.isFile() then 3 else if stats.isDirectory() then 5 else 1
         o 'getLastModifiedTime(Ljava/io/File;)J', (rs, _this, file) ->
-            stats = stat_file rs.jvm2js_str file.fields.path
-            util.java_throw 'java/io/FileNotFoundException' unless stats?
+            filepath = rs.jvm2js_str file.fields.path
+            stats = stat_file filepath
+            util.java_throw(rs, 'java/io/FileNotFoundException', "Could not stat file #{filepath}") unless stats?
             gLong.fromNumber (new Date(stats.mtime)).getTime()
         o 'canonicalize0(L!/lang/String;)L!/lang/String;', (rs, _this, jvm_path_str) ->
             js_str = rs.jvm2js_str jvm_path_str


remove fonts

remove fonts

--- src/options.coffee
+++ src/options.coffee
@@ -386,10 +386,6 @@
       { texname: 'mktsFontfileSourcesansprosemibold',           subfolder: 'Source_Sans_Pro',         filename: 'SourceSansPro-Semibold.ttf'          }
       { texname: 'mktsFontfileSourcesansprosemibolditalic',     subfolder: 'Source_Sans_Pro',         filename: 'SourceSansPro-SemiboldItalic.ttf'    }
       { texname: 'mktsFontfileStintultracondensedregular',      subfolder: 'Stint_Ultra_Condensed',   filename: 'StintUltraCondensed-Regular.ttf'     }
-      { texname: 'mktsFontfileYanonekaffeesatzbold',            subfolder: 'Yanone_Kaffeesatz',       filename: 'YanoneKaffeesatz-Bold.ttf'           }
-      { texname: 'mktsFontfileYanonekaffeesatzextralight',      subfolder: 'Yanone_Kaffeesatz',       filename: 'YanoneKaffeesatz-ExtraLight.ttf'     }
-      { texname: 'mktsFontfileYanonekaffeesatzlight',           subfolder: 'Yanone_Kaffeesatz',       filename: 'YanoneKaffeesatz-Light.ttf'          }
-      { texname: 'mktsFontfileYanonekaffeesatzregular',         subfolder: 'Yanone_Kaffeesatz',       filename: 'YanoneKaffeesatz-Regular.ttf'        }
       { texname: 'mktsFontfilePermianslabseriftypeface',        subfolder: 'Permian_Slab',            filename: 'PermianSlabSerifTypeface.otf',        }
       { texname: 'mktsFontfilePermianslabseriftypefacebold',    subfolder: 'Permian_Slab',            filename: 'PermianSlabSerifTypeface-Bold.otf',   }
       { texname: 'mktsFontfilePermianslabseriftypefaceitalic',  subfolder: 'Permian_Slab',            filename: 'PermianSlabSerifTypeface-Italic.otf', }


Replace all occurences of capturing group placeholders in scope name pattern.

--- src/pattern.coffee
+++ src/pattern.coffee
@@ -1,7 +1,7 @@
 _ = require 'underscore-plus'

+AllCustomCaptureIndicesRegex = /\$(\d+)|\${(\d+):\/(downcase|upcase)}/g
 AllDigitsRegex = /\\\d+/g
-CustomCaptureIndexRegex = /\$(\d+)|\${(\d+):\/(downcase|upcase)}/
 DigitRegex = /\\\d+/

 module.exports =
@@ -119,7 +119,7 @@
       [this]

   resolveScopeName: (scopeName, line, captureIndices) ->
-    resolvedScopeName = scopeName.replace CustomCaptureIndexRegex, (match, index, commandIndex, command) ->
+    resolvedScopeName = scopeName.replace AllCustomCaptureIndicesRegex, (match, index, commandIndex, command) ->
       capture = captureIndices[parseInt(index ? commandIndex)]
       if capture?
         replacement = line.substring(capture.start, capture.end)


Make the source tracker stack valid property its own variable

--- src/observable/property.coffee
+++ src/observable/property.coffee
@@ -2,12 +2,13 @@
 #= require ../event_emitter/event_emitter
 #= require ../set/simple_set

+SOURCE_TRACKER_STACK = []
+SOURCE_TRACKER_STACK_VALID = true

 class Batman.Property
   Batman.mixin @prototype, Batman.EventEmitter

-  @_sourceTrackerStack: []
-  @_sourceTrackerStack.valid = true
+  @_sourceTrackerStack: SOURCE_TRACKER_STACK
   @defaultAccessor:
     get: (key) -> @[key]
     set: (key, val) -> @[key] = val
@@ -38,30 +39,30 @@
         Batman.Property.popSourceTracker()
   @registerSource: (obj) ->
     return unless obj.isEventEmitter
-    if !Batman.Property._sourceTrackerStack.valid
+    if !SOURCE_TRACKER_STACK_VALID
       set = []
-      Batman.Property._sourceTrackerStack.push set
-      Batman.Property._sourceTrackerStack.valid = true
-    else
-      set = Batman.Property._sourceTrackerStack[Batman.Property._sourceTrackerStack.length - 1]
+      SOURCE_TRACKER_STACK.push set
+      SOURCE_TRACKER_STACK_VALID = true
+    else
+      set = SOURCE_TRACKER_STACK[SOURCE_TRACKER_STACK.length - 1]

     set?.push(obj)
     undefined

   @pushSourceTracker: ->
-    if !Batman.Property._sourceTrackerStack.valid
-      Batman.Property._sourceTrackerStack.push []
-    else
-      Batman.Property._sourceTrackerStack.valid = false
+    if !SOURCE_TRACKER_STACK_VALID
+      SOURCE_TRACKER_STACK.push []
+    else
+      SOURCE_TRACKER_STACK_VALID = false
   @popSourceTracker: ->
-    if !Batman.Property._sourceTrackerStack.valid
-      Batman.Property._sourceTrackerStack.valid = true
+    if !SOURCE_TRACKER_STACK_VALID
+      SOURCE_TRACKER_STACK_VALID = true
       undefined
     else
-      Batman.Property._sourceTrackerStack.pop()
+      SOURCE_TRACKER_STACK.pop()

   @pushDummySourceTracker: ->
-    Batman.Property._sourceTrackerStack.push(null)
+    SOURCE_TRACKER_STACK.push(null)

   constructor: (@base, @key) ->
   _isolationCount: 0


add comment for arc instability (see #7)


make sure that no results text is shown

make sure that no results text is shown

--- src/ajax-chosen.coffee
+++ src/ajax-chosen.coffee
@@ -110,9 +110,14 @@
                   .html(element)
                   .appendTo(select)

-            # Tell chosen that the contents of the <select> input have been updated
-            # This makes chosen update its internal list of the input data.
-            select.trigger("liszt:updated")
+            if Object.keys(items).length
+              # Tell chosen that the contents of the <select> input have been updated
+              # This makes chosen update its internal list of the input data.
+              select.trigger("liszt:updated")
+            else
+              # If there are no results, display the no_results text
+              select.data().chosen.no_results_clear()
+              select.data().chosen.no_results field.attr('value')

             # Finally, call the user supplied callback (if it exists)
             success(data) if success?


Changed exported object from request.coffee module

Now only exports one function.

--- src/request.coffee
+++ src/request.coffee
@@ -8,7 +8,7 @@
       memo.push obj[predicate]
       break
     else
-      findNested(val, predicate, memo) if _(val).isObject
+      findNested(val, predicate, memo) if _(val).isObject()
   if memo.length then _.first(_.flatten(memo)) else null

 isError = (errorCode) ->
@@ -26,11 +26,9 @@
     [err, data] = [createErrorMsg(body, 'errorCode', 'errorText'), body]
   [err, data]

-module.exports =
-
-  fetch: (url, raw, deferred) ->
-    request { url }, (err, response, body) ->
-      unless err or raw
-        [err, body] = parseResponse body
-      if err then deferred.reject err
-      else deferred.resolve body
+module.exports = (url, raw, deferred) ->
+  request { url }, (err, response, body) ->
+    unless err or raw
+      [err, body] = parseResponse body
+    if err then deferred.reject err
+    else deferred.resolve body


own it

own it

--- src/require.coffee
+++ src/require.coffee
@@ -1,6 +1,8 @@
 # Hack to get code reloading working in dev mode
 if OSX.NSUserName().valueOf() == "corey"
   resourcePath = "/Users/corey/code/mine/Atomicity"
+else if OSX.NSUserName().valueOf() == "chris"
+  resourcePath = "/Users/chris/Code/Atomicity"
 else
   resourcePath = OSX.NSBundle.mainBundle.resourcePath


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -1,5 +1,5 @@
 ClampBounds = (I={}, self) ->
-  Obje
+  Object.reverseMerg

   self.bind "update", ->
     I.x = I.x.clamp()


ClampBounds docs

ClampBounds docs

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -25,7 +25,7 @@
   # => 2.5 # half the player's width

   player.I.y
-  # => 311.5 # The default App.height (320) - half the play
+  # => 311.5 # The default App.height (320) - half the player's height


Fixed background rendering (partially).

Fixed background rendering (partially).

--- src/PPU.coffee
+++ src/PPU.coffee
@@ -30,7 +30,7 @@
     @inject: [ "ppuMemory", "cpu" ]

     constructor: ->
-        @framebufferPosition = 0
+        @framePosition = 0
         @framebuffer = for i in [0 ... WIDTH * HEIGHT * 4]
             if (i & 0x03) != 0x03 then 0x00 else 0xFF # RGBA = 00.00.00.FF

@@ -182,19 +182,20 @@
     isPalleteAddress: (address) ->
         (address & 0x3F00) == 0x3F00

-    getMonochromeColor: (index) ->
-        index & 0x30
+    getMonochromeColor: (color) ->
+        color & 0x30

     ###########################################################
     # Scrolling
     ###########################################################

-    # Uses the same register as VRAM addressing.
-    # The register has the following structure: $0yyy.NNYY.YYYX.XXXX
-    #   yyy = fine Y scroll (y position of active row of rendered tile)
-    #    NN = index of active name table
-    # YYYYY = coarse Y scroll (y position of rendered tile in name table)
-    # XXXXX = coarse X scroll (x position of rendered tile in name table)
+    # The position of currently rendered pattern and its pixel is stored in
+    # VRAM address register with following structure: 0yyy.NNYY.YYYX.XXXX.
+    #     yyy = fine Y scroll (y pixel position within pattern)
+    #      NN = index of active name table (where pattern numbers are stored)
+    #   YYYYY = coarse Y scroll (y position of pattern number in name table)
+    #   XXXXX = coarse X scroll (x position of pattern number in name table)
+    # Fine X scroll (x pixel position within pattern) has its own register.

     writeScroll: (value) ->
         @writeToogle = not @writeToogle
@@ -267,66 +268,72 @@

     renderFramePixel: ->
         @frameStart = @scanline is 0 and @cycle is 1
-        @framebufferPosition = 0 if @frameStart
+        @framePosition = 0 if @frameStart
         backgroundColor = @renderBackgroundPixel() if @backgroundVisible
         spriteColor = @renderSpritePixel() if @spritesVisible
-        @setFramePixel spriteColor or backgroundColor or 0 # TODO rendering priority
+        @setFramePixel backgroundColor or 0 # TODO rendering priority
+
+    # Colors are saved at addresses with structure 0111.1111.000S.PPCC.
+    #    S = 0 for background, 1 for sprites
+    #   PP = palette number
+    #   CC = color number
+    #
+    # The position of currently rendered pattern and its pixel is stored in
+    # VRAM address register with following structure: 0yyy.NNYY.YYYX.XXXX.
+    #     yyy = fine Y scroll (y pixel position within pattern)
+    #      NN = index of active name table (where pattern numbers are stored)
+    #   YYYYY = coarse Y scroll (y position of pattern number in name table)
+    #   XXXXX = coarse X scroll (x position of pattern number in name table)
+    # Fine X scroll (x pixel position within pattern) has its own register.
+    # Address of a pattern number can be constructed as 0010.NNYY.YYYX.XXXX.
+    #
+    # Pattern number used as an offset into one of pattern tables at
+    # 0x0000 and 0x1000, where patterns are stored. We can construct
+    # this address as 00T.0000.PPPP.0000
+    #      T = pattern table selection bit
+    #   PPPP = pattern number
+    #
+    # Each pattern is 16B long and consits from two 8x8 matricies.
+    # Each 8x8 matrix contains 1 bit of CC (color number) for pattern pixels.
+    #
+    # Palette numbers PP are defined for 2x2 tile areas. These numbers
+    # are store as attributes in attribute tables. Each attribute (1B) contains
+    # total 4 palette numbers for bigger 4x4 tile area as value 3322.1100
+    #   33 = palette number for bottom right 2x2 area.
+    #   22 = palette number for bottom left 2x2 area.
+    #   11 = palette number for top right 2x2 area
+    #   00 = palette number for top left 2x2 area.
+    # Address of an attribute can be constructed as 0010.NN11.11YY.YXXX

     renderBackgroundPixel: ->
-        # Optimization - we compute this only when coarse scroll X was incremented.
-        if @fineXScroll is 0 or @frameStart
-            # VRAM address register following structure: $0yyy.NNYY.YYYX.XXXX
-            #   yyy = fine Y scroll (y position of active row of rendered tile)
-            #    NN = index of active name table
-            # YYYYY = coarse Y scroll (y position of rendered tile in name table)
-            # XXXXX = coarse X scroll (x position of rendered tile in name table)
-            # $2.NNYY.YYYX.XXXX is pointer into active name table,
-            # where is saved currently rendered background tile (pattern number).
-            patternNumer = @read 0x2000 | (@vramAddress & 0x0FFF)
-            # Based on control bit, we select 1 of 2 pattern tables (0x0000 or 0x1000).
-            # Then we find address of pattern inside this table (each pattern has 16B).
-            patternTableAddress = @backgroundPatternTableIndex << 24
+        if @fineXScroll is 0 or @frameStart # When coarse scroll X was incremented.
+            patternNumer = @read 0x2000 | @vramAddress & 0x0FFF
+            patternTableAddress = @backgroundPatternTableIndex << 12
             patternAddress = patternTableAddress + (patternNumer << 4)
-            # Each pattern consists from two 8x8 bit matrices (total 16B).
-            # Each 8x8 matrix is a bitmap defining 1 bit of a color of pattern's pixels.
-            # We read 1 bit from each bitmap on position specified by fine X, Y scroll.
             fineYScroll = (@vramAddress >>> 12) & 0x07
-            colorBit1RowAddress = patternAddress + fineYScroll
-            colorBit2RowAddress = colorBit1RowAddress + 8
-            @colorBit1Row = @read colorBit1RowAddress
-            @colorBit2Row = @read colorBit2RowAddress
-        colorBitNumber = @fineXScroll ^ 0x07 # 7 - fineXScroll
-        colorBit1 =  (@colorBit1Row >>> colorBitNumber) & 0x01
-        colorBit2 = ((@colorBit2Row >>> colorBitNumber) & 0x01) << 1
-        # Optimization - we compute this only when coarse scroll X was incremented 4 times.
-        if (@vramAddress & 0x0003) is 0 or @frameStart
-            # We compute pointer to attribute table as $2.NN11.11YY.Y.XXX
-            # where YYY and XXX are 3 upper bits of YYYYY and XXXXX.
-            # Each attribute applies to area of 4x4 tiles (that's why we removed 2 lower bits of YYYYY and XXXXX).
+            @layer1Row = @read patternAddress + fineYScroll
+            @layer2Row = @read patternAddress + fineYScroll + 8
+        bitNumber = @fineXScroll ^ 0x07 # 7 - fineXScroll
+        colorSelect1 =  (@layer1Row >>> bitNumber) & 0x01
+        colorSelect2 = ((@layer2Row >>> bitNumber) & 0x01) << 1
+        if (@vramAddress & 0x0003) is 0 or @frameStart # When coarse scroll X was incremented 4 times.
             attributeTableAddress = 0x23C0 | (@vramAddress & 0x0C00)
             attributeNumber = (@vramAddress >>> 4) & 0x38 | (@vramAddress >>> 2) & 0x07
-            attributeAddress = attributeTableAddress + attributeNumber
-            attribute = @read attributeAddress
-            # Attribute has format $3322.1100. where 00, 11, 22, 33 contains palette number
-            # for one of 2x2 subarea of the 4x4 tile area.
-            # We have to find in which subarea are we in. This is decided by bit 1 of YYYYY and XXXXX values.
-            subareaNumber = (@vramAddress >>> 5) & 0x02 | (@vramAddress >>> 1) & 0x01
-            @paletteOffset = ((attribute >>> subareaNumber) & 0x03) << 2
-        # The result color address has the following structure : 111.1111.0000.PPCC
-        # PP = palette number
-        # CC = color number
-        @read 0x3F00 | @paletteOffset | colorBit2 | colorBit1
+            attribute = @read attributeTableAddress + attributeNumber
+            areaNumber = (@vramAddress >>> 5) & 0x02 | (@vramAddress >>> 1) & 0x01
+            @paletteSelect = ((attribute >>> areaNumber) & 0x03) << 2
+        @read 0x3F00 | @paletteSelect | colorSelect2 | colorSelect1

     renderSpritePixel: ->
         color = 0 # TODO implement sprite rendering
         @read 0x3F10 + color

     setFramePixel: (color) ->
-        color <<= 2 # Each RGBA color is 4B
-        @framebuffer[@framebufferPosition++] = RGBA_COLORS[color++]
-        @framebuffer[@framebufferPosition++] = RGBA_COLORS[color++]
-        @framebuffer[@framebufferPosition++] = RGBA_COLORS[color]
-        @framebufferPosition++ # Alpha is always 0xFF
+        colorPosition = color << 2 # Each RGBA color is 4B.
+        @framebuffer[@framePosition++] = RGBA_COLORS[colorPosition++]
+        @framebuffer[@framePosition++] = RGBA_COLORS[colorPosition++]
+        @framebuffer[@framePosition++] = RGBA_COLORS[colorPosition]
+        @framePosition++ # Skip alpha because it was already set to 0xFF.

     updateScrolling: ->
         @incrementFineYScroll() if @cycle is 256
@@ -384,9 +391,9 @@
             for columnNumber in [0...8]
                 x = baseX + columnNumber
                 bitPosition = columnNumber ^ 0x07
-                offsetBit1 =  (layer1Row >> bitPosition) & 0x01
-                offsetBit2 = ((layer2Row >> bitPosition) & 0x01) << 1
-                color = @read 0x3F00 | offsetBit1 | offsetBit2
+                colorSelect1 =  (layer1Row >> bitPosition) & 0x01
+                colorSelect2 = ((layer2Row >> bitPosition) & 0x01) << 1
+                color = @read 0x3F00 | colorSelect2 | colorSelect1
                 @setFramePixelOnPosition x, y, color

     renderPalettes: ->


remove onClick handler from link

remove onClick handler from link

--- src/components/app.cjsx
+++ src/components/app.cjsx
@@ -117,7 +117,7 @@
                 onConfirm={@onReset}
                 {...guardProps}
               >
-                Reset
+                Reset
               </SuretyGuard>
               <SuretyGuard
                 onConfirm={@onNewBlank}


Adjusted references for logging

Adjusted references for logging

--- src/api.coffee
+++ src/api.coffee
@@ -70,15 +70,15 @@
         @sock.on 'close', =>
           if @autoReconnect == true and @initiatedClose != true
             @sock = null
-            @connect().then ->
-              console.log "Push WebSocket replaced for namespace '#{data.record.namespace}' topic [#{_(data.record.attributes).join(",")}]"
-            .catch (error) ->
-              console.error "Error replacing push WebSocket for namespace '#{data.record.namespace}' topic [#{_(data.record.attributes).join(",")}] : ${error}\n${error.stack}"
+            @connect().then =>
+              console.log "Push WebSocket replaced for namespace '#{@namespace}' topic [#{_(@attributes).join(",")}]"
+            .catch (error) =>
+              console.error "Error replacing push WebSocket for namespace '#{@namespace}' topic [#{_(@attributes).join(",")}] : ${error}\n${error.stack}"

             @emit 'reconnect'
           else
             @emit 'close'
-            console.log "Push WebSocket closed for namespace '#{data.record.namespace}' topic [#{_(data.record.attributes).join(",")}]"
+            console.log "Push WebSocket closed for namespace '#{@namespace}' topic [#{_(@attributes).join(",")}]"

         # The WebSocket connection has been established
         @sock.on 'open', =>
@@ -96,7 +96,7 @@
         @sock.on 'error', (error) =>
           @emit 'error', error

-          console.error "WebSocket error for namespace '#{data.record.namespace}' topic [#{_(data.record.attributes).join(",")}] : #{error}\n#{error.stack}"
+          console.error "WebSocket error for namespace '#{@namespace}' topic [#{_(@attributes).join(",")}] : #{error}\n#{error.stack}"

         # Received a message
         @sock.on 'message', (msg) =>


updating api to use new publish data and updating to use uid instead of id

--- src/api.coffee
+++ src/api.coffee
@@ -66,8 +66,10 @@

 start = ->
   apiHelper ExerciseActions, ExerciseActions.load, ExerciseActions.loaded, 'GET', (id) ->
-    url: "/api/exercises/#{id}"
-
+    if id.indexOf("@") is -1
+      url: "/api/exercises/#{id}@draft"
+    else
+      url: "/api/exercises/#{id}"

   apiHelper ExerciseActions, ExerciseActions.save, ExerciseActions.saved, 'PUT', (id) ->

@@ -75,7 +77,7 @@
     obj = ExerciseStore.getChanged(id)
     obj.exercise = ExerciseStore.get(id)

-    url: "/api/exercises/#{id}"
+    url: "/api/exercises/#{id}@draft"
     httpMethod: 'PUT'
     payload: obj

@@ -83,8 +85,9 @@
   apiHelper ExerciseActions, ExerciseActions.publish, ExerciseActions.saved, 'PUT', (id) ->

     obj = ExerciseStore.get(id)
+    uid = ExerciseStore.getId(id)

-    url: "/api/exercises/#{id}/publish"
+    url: "/api/exercises/#{uid}/publish"
     httpMethod: 'PUT'
     payload: obj


escape key clears selection

escape key clears selection

--- src/app.coffee
+++ src/app.coffee
@@ -17,6 +17,8 @@
 base_title = document.title
 userIsSeeking = false
 userIsVolumeSliding = false
+started_drag = false
+abortDrag = null
 MARGIN = 10

 renderPlaylist = ->
@@ -252,7 +254,6 @@

       # dragging
       if not skip_drag
-        started_drag = false
         start_drag_x = event.pageX
         start_drag_y = event.pageY

@@ -286,14 +287,10 @@
           $(document)
             .off('mousemove', onDragMove)
             .off('mouseup', onDragEnd)
-            .off('keydown', onKeyDown)

           if started_drag
             $playlist.find(".pl-item").removeClass('border-top').removeClass('border-bottom')
-
-        onKeyDown = (event) ->
-          if event.keyCode == 27
-            abortDrag()
+            started_drag = false

         onDragMove = (event) ->
           if not started_drag
@@ -305,8 +302,6 @@
           $("#playlist-track-#{result.track_id}").addClass "border-#{result.direction}"

         onDragEnd = (event) ->
-          abortDrag()
-
           if started_drag
             result = getDragPosition(event.pageX, event.pageY)
             delta =
@@ -314,16 +309,17 @@
               bottom: 1
             new_pos = mpd.playlist.item_table[result.track_id].pos + delta[result.direction]
             mpd.moveIds (id for id of selection.playlist_ids), new_pos
+
           else
             # we didn't end up dragging, select the item
             (selection.playlist_ids = {})[track_id] = true
             selection.cursor = track_id
             refreshSelection()
+          abortDrag()

         $(document)
           .on('mousemove', onDragMove)
           .on('mouseup', onDragEnd)
-          .on('keydown', onKeyDown)

         onDragMove event

@@ -385,7 +381,18 @@

     handlers =
       # escape
-      27: removeContextMenu
+      27: ->
+        # if the user is dragging, abort the drag
+        if started_drag
+          abortDrag()
+          return
+        # if there's a menu, only remove that
+        if $("#menu").get().length > 0
+          removeContextMenu()
+          return
+        # clear selection
+        selection.type = null
+        refreshSelection()
       # space
       32: togglePlayback
       # up


clean the code

clean the code

--- src/app.coffee
+++ src/app.coffee
@@ -1,5 +1,5 @@
 chalk      = require 'chalk'
-commander  = require 'commander'
+commander  = new (require('commander').Command)('yomano')
 ejs        = require 'ejs'
 {execSync} = require 'child_process'
 fs         = require 'fs'
@@ -30,10 +30,8 @@

     options.escape = (input) -> jsesc input, quotes:'double'

-    block = /// ^ %%% (.+) $ ///gm
+    block    = /// ^ %%% (.+) $ ///gm
     template = input.replace block, (m, b) -> "<\%_ #{b} -\%>"
-
-    console.log template

     ejs.render template, data, options

@@ -73,8 +71,7 @@
         if Array.isArray r
             for l in r
                 if typeof l is 'function'
-                    r2 = l()
-                    exec l2 for l2 in r2 if Array.isArray r2
+                    exec l2 for l2 in r2 if Array.isArray (r2 = l())
                 else
                     exec l
     return
@@ -83,12 +80,11 @@
 config = new MyConfig()

 context =
-    platform: process.platform
-    dest: process.cwd()
+    platform : process.platform
+    dest     : process.cwd()
+    filters  : ['**/*']
     date:
         year: new Date().getFullYear()
-    # filters: ['**/*', '**/.*']
-    filters: ['**/*']

 commander
     .version require('../package.json').version
@@ -100,9 +96,9 @@
     .option '-f, --force', 'force instalation into non empty folder'
     .option '-v, --verbose', 'verbose mode'
     .action (pack, options) ->
-        context._verbose = options.verbose
-        context._save = options.save
-        context._force = options.force
+        context._verbose  = options.verbose
+        context._save     = options.save
+        context._force    = options.force
         context.pack_name = pack
         context.pack_file = "yomano-#{pack}"
     # .on '--help', -> console.log """"""
@@ -113,6 +109,13 @@
     .action ->
         context.pack_name = 'new'
         context.pack_file = "../init"
+
+commander
+    .command 'task <task_name>'
+    .description 'execute a task on a yomano project'
+    .action (task, options) ->
+        console.log '\n\nto be implemented\n'
+        process.exit 1

 commander
     .command 'home [path]'
@@ -130,13 +133,13 @@
     process.exit 1 unless positive "#{chalk.red('Oops!')} Target folder already have files inside. Continue? [No]: ", no

 try
-    pack = require(context.pack_file)(chalk, fs, path)
+    pack           = require(context.pack_file)(chalk, fs, path)
     context.source = path.join path.dirname(require.resolve context.pack_file), 'source'

 if not context.source? and config.get 'home'
     try
-        local = path.join config.get('home'), context.pack_file
-        pack = require(local)(chalk)
+        local          = path.join config.get('home'), context.pack_file
+        pack           = require(local)(chalk)
         context.source = path.join path.dirname(require.resolve local), 'source'

 unless context.source?
@@ -147,18 +150,18 @@
 executeEvents pack.init

 base_prompt = [
-    name: "name"
-    message: 'Application name'
-    validate: (v) -> if /^[\w-]{2,}$/.exec v then true else "Invalid or too short"
-    default: path.basename process.cwd()
+    name     : "name"
+    message  : 'Application name'
+    validate : (v) -> if /^[\w-]{2,}$/.exec v then true else "Invalid or too short"
+    default  : path.basename process.cwd()
 ,
-    name: "owner"
-    message: "your name"
-    default: config.get 'owner'
+    name    : "owner"
+    message : "your name"
+    default : config.get 'owner'
 ,
-    name: "email"
-    message: "your email"
-    default: config.get 'email'
+    name    : "email"
+    message : "your email"
+    default : config.get 'email'
 ]

 inquirer.prompt if pack.prompt? then base_prompt.concat(pack.prompt) else base_prompt
@@ -194,13 +197,13 @@

         target = file

-        test = /// \( ([+-])? ([a-z0-9]+) \) ///g
+        test    = /// \( ([+-])? ([a-z0-9]+) \) ///g
         install = yes
         while (m = test.exec file)?
             if m[2] of context
                 install = install && context[m[2]] if m[1] == '+' || not m[1]?
                 install = install && !context[m[2]] if m[1] == '-'
-                target = target.replace m[0], ''
+                target  = target.replace m[0], ''

         (bar.tick(); continue) unless install

@@ -215,7 +218,7 @@

         for o in opt
             if o[..6] == 'rename:'
-                val = o[7..].split ':'
+                val    = o[7..].split ':'
                 target = target.replace val[0], val[1]

         copyFile path.join(context.source, file), path.join(context.dest, target), context, opt, -> bar.tick()


Updated app.coffee to AMD

Updated app.coffee to AMD

--- src/app.coffee
+++ src/app.coffee
@@ -1,6 +1,7 @@
-do (window = @, Elyssa = @Elyssa or= {}) ->
+define 'elyssa/app', ->
+  'use strict'

-  class Elyssa.App
+  class App
     constructor: ->


Only print the board games

Only print the board games

--- src/bgg.coffee
+++ src/bgg.coffee
@@ -26,7 +26,10 @@

 module.exports = (robot) ->
   robot.respond /bgg search (.*)/, (msg) ->
-    bgg('search', query: msg.match[1])
+    params =
+      query: msg.match[1]
+      type: 'boardgame'
+    bgg('search', params)
       .then (results) ->
         items = results.items.item
         for item in items


Track and clear the presence interval timer; make the timer period configurable.

--- src/bot.coffee
+++ src/bot.coffee
@@ -42,8 +42,10 @@
     @lastHeartbeat   = Date.now()
     @lastActivity    = Date.now()
     @clientId        = Date.now() + '-0.59633534294921572'
+    @presenceInterval = 10000
     @_msgId          = 0
     @_cmds           = []
+    @_intervalId     = null
     @_isAuthenticated = false
     @_isConnected    = false
     @fanOf           = []
@@ -144,7 +146,9 @@
           @fanOf = data.fanof
           @updatePresence()
           # TODO: I don't like setInterval !
-          setInterval(@updatePresence.bind(@), 10000)
+          if @_intervalId
+            clearInterval(@_intervalId)
+          @_intervalId = setInterval(@updatePresence.bind(@), @presenceInterval)
           @emit 'ready'
       @callback()
       @_isConnected = true


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_flash.coffee
+++ src/engine_flash.coffee
@@ -30,6 +30,17 @@
   A convenient way to set the flash effect instance variables. Alternatively, you can modify them by hand, but
   using Engine#flash is the suggested approach.

+

+  engine.flash()
+  # => Sets the flash effect variables to their default state. This will cause a white flash that will turn transparent in the next 12 frames.
+
+  engine.flash('green', 30)
+  # => This flash effect will start off green and fade to transparent over 30 frames.
+
+  engine.flash(Color(255, 0, 0, 0), 20, 1)
+  # => This flash effect will start off transparent and move toward red over 20 frames
+

+
   @name flash
   @methodOf Engine#
   @param {Color} [color="white"] The flash color


Fix for workaround for users type parsing

--- src/hubot/talker.coffee
+++ src/hubot/talker.coffee
@@ -72,10 +72,10 @@
     @socket.addListener 'data', (data) ->
       console.log data

-      if data.indexOf('{"type":"users"') == 0
+      if data.indexOf('"type":"users"') > 0
+        message = {"type": "none"}
+      else
         message = JSON.parse(data)
-      else
-        message = {"type": "none"}

       if message.type == "connected"
         console.log "Succesfully connected, listing users:"


Tweak -o argument doc

Tweak -o argument doc

--- src/cli.coffee
+++ src/cli.coffee
@@ -6,7 +6,7 @@
 module.exports = ->
   cli = optimist.describe('h', 'Show this message').alias('h', 'help')
                 .describe('s', 'Scope name of the grammar to use').alias('s', 'scope').string('s')
-                .describe('o', 'File path to write HTML output to').alias('o', 'output').string('o')
+                .describe('o', 'File path to write the HTML output to').alias('o', 'output').string('o')
   optimist.usage """
     Usage: highlights [options] [file]


Make source maps not default.

Make source maps not default.

--- src/cli.coffee
+++ src/cli.coffee
@@ -30,7 +30,7 @@
     -o, --output <file>          write bundle to file instead of stdout, {} may be used as a placeholder
     -p, --prelude <file>         file to use as prelude
         --no-prelude             exclude prelude from bundle
-        --no-source-map          disable source maps
+        --source-map             enable source maps
         --prelude-only           only output prelude
     -r, --resolve <module:path>  do not automatically resolve module, use provided path
     -s, --strict                 add "use strict" to each bundled module
@@ -66,7 +66,7 @@
   output:     []
   prelude:    null
   resolveAs:  {}
-  sourceMap:  true
+  sourceMap:  false
   strict:     false
   stripDebug: false
   watch:      false
@@ -105,8 +105,8 @@
       opts.prelude = args.shift()
     when '--no-prelude'
       opts.prelude = false
-    when '--no-source-map'
-      opts.sourceMap = false
+    when '--source-map'
+      opts.sourceMap = true
     when '--prelude-only'
       opts.preludeOnly = true
     when '-r', '--resolve', '--resolve-as'


Minor fix for r2_calculator tests

Minor fix for r2_calculator tests

Prevents a highlighting error on the require line

--- spec/javascripts/r2_calculator_spec.coffee
+++ spec/javascripts/r2_calculator_spec.coffee
@@ -1,4 +1,4 @@
-//=require 'income_calculator'
+#=require 'income_calculator'

 runScenario=(t) ->
   it "id #{t.id} passes", ->


Remove old Atom file

Remove old Atom file

--- atom.cson
+++ atom.cson
@@ -1,72 +0,0 @@
-"*":
-  core:
-    autoHideMenuBar: true
-    customFileTypes:
-      "source.json": [
-        ".yaspellerrc"
-        ".stylelintrc"
-        ".eslintrc"
-      ]
-    disabledPackages: [
-      "language-objective-c"
-      "autosave"
-      "bookmarks"
-      "language-clojure"
-      "language-perl"
-      "language-php"
-      "welcome"
-      "spell-check"
-      "deprecation-cop"
-      "timecop"
-      "styleguide"
-      "github"
-      "go-to-line"
-      "language-csharp"
-      "language-go"
-      "language-hyperlink"
-      "language-mustache"
-      "autocomplete-atom-api"
-      "language-sql"
-      "language-property-list"
-      "open-on-github"
-      "package-generator"
-      "whitespace"
-    ]
-    projectHome: "/home/ai/Dev"
-    telemetryConsent: "limited"
-    themes: [
-      "nucleus-dark-ui"
-      "atom-panda-syntax"
-    ]
-  editor:
-    fontFamily: "JetBrains Mono"
-    fontSize: 18
-    invisibles: {}
-    nonWordCharacters: "^*\"`'/|\\?!;:,.%#@(){}[]<>=+-~_"
-    showIndentGuide: true
-    softWrap: true
-    tabLength: 2
-    zoomFontWhenCtrlScrolling: false
-  "exception-reporting":
-    userId: "51ccaaae-04bc-1ed3-3445-f9bd1a2f3a71"
-  linter:
-    lintOnChange: false
-  "linter-eslint": {}
-  "linter-stylelint-plus":
-    fixOnSave: true
-  "linter-ui-default":
-    panelHeight: 192.75
-    showPanel: true
-  "markdown-preview":
-    breakOnSingleNewline: true
-  minimap:
-    displayPluginsControls: false
-    plugins:
-      "find-and-replace": true
-      "find-and-replaceDecorationsZIndex": 0
-      linter: true
-      linterDecorationsZIndex: 0
-  "tree-view": {}
-".pug.source":
-  editor:
-    nonWordCharacters: "^*\"`'/|\\?!;:,.%#@(){}[]<>=+-~_"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/flickerable.coffee
+++ test/flickerable.coffee
@@ -6,7 +6,7 @@

   player.include Flickerable

-  equals flicker.player.I.alpha, 0.9
+  equals player.I.alpha, 0.9

 module()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/game_object.coffee
+++ test/game_object.coffee
@@ -70,8 +70,8 @@

 test "[event] destroy", 2, ->
   o = GameObject
-  o.
-    destroy: ->
+  o.bind "destroy"
+    : ->
       ok true, "destroyed event is fired on destroy"
   .destroy()


:white_check_mark: Add test for emission's action

--- specs/units/particles/emission.spec.coffee
+++ specs/units/particles/emission.spec.coffee
@@ -5,6 +5,7 @@
 ByRate = agt.particles.counters.ByRate
 Ponctual = agt.particles.emitters.Ponctual
 Life = agt.particles.initializers.Life
+Live = agt.particles.actions.Live

 describe 'Emission', ->
   describe 'when instanciated with all components', ->
@@ -13,8 +14,8 @@
       @timer = timer = new Limited(1000)
       @counter = counter = new ByRate(10)
       @initializer = initializer = new Life(100)
-      @initializer = initializer = new Life(100)
-      @emission = new Emission({class: Particle, emitter, timer, counter, initializer})
+      @action = action = new Live()
+      @emission = new Emission({class: Particle, emitter, timer, counter, initializer, action})

     it 'should have stored the passed-in arguments', ->
       expect(@emission.class).toBe(Particle)
@@ -22,6 +23,7 @@
       expect(@emission.timer).toBe(@timer)
       expect(@emission.counter).toBe(@counter)
       expect(@emission.initializer).toBe(@initializer)
+      expect(@emission.action).toBe(@action)

     describe 'when its prepare method is called', ->
       beforeEach -> @emission.prepare 500, 0.5, 500


:lipstick:

:lipstick:

--- src/app/project.coffee
+++ src/app/project.coffee
@@ -140,7 +140,7 @@
     regex = new RegExp(regex.source, 'g')
     command = "#{require.resolve('ack')} --all --match \"#{regex.source}\" \"#{@getPath()}\""
     bufferedData = ""
-    promise = ChildProcess.exec command , bufferLines: true, stdout: (data) ->
+    ChildProcess.exec command , bufferLines: true, stdout: (data) ->
       bufferedData += data
       currentIndex = 0
       while currentIndex < bufferedData.length


do not override dockerfile cmd

do not override dockerfile cmd

--- src/application.coffee
+++ src/application.coffee
@@ -101,7 +101,9 @@
 				updateDeviceInfo(status: 'Downloading')
 				dockerUtils.fetchImageWithProgress app.imageId, (progress) ->
 					updateDeviceInfo(download_progress: progress.percentage)
-			.then ->
+				.then ->
+					docker.getImage(app.imageId).inspectAsync()
+			.then (imageInfo) ->
 				console.log('Creating container:', app.imageId)
 				updateDeviceInfo(status: 'Starting')
 				ports = {}
@@ -109,9 +111,14 @@
 					portList.forEach (port) ->
 						ports[port + '/tcp'] = {}

+				if imageInfo?.Config?.Cmd
+					cmd = imageInfo.Config.Cmd
+				else
+					cmd = [ '/bin/bash', '-c', '/start' ]
+
 				docker.createContainerAsync(
 					Image: app.imageId
-					Cmd: [ '/bin/bash', '-c', '/start' ]
+					Cmd: cmd
 					Tty: true
 					Volumes:
 						'/data': {}


`mixpanelTrack` expects the error property to be called `error`

--- src/application.coffee
+++ src/application.coffee
@@ -65,14 +65,14 @@
 		eventName: 'Application update'
 		humanName: 'Updating application'

-logSystemEvent = (logType, app, err) ->
+logSystemEvent = (logType, app, error) ->
 	message = "#{logType.humanName} '#{app.imageId}'"
-	if err?
+	if error?
 		# Report the message from the original cause to the user.
-		errMessage = err.cause?.json ? err.cause?.message ? err.message
+		errMessage = error.cause?.json ? error.cause?.message ? error.message
 		message += " due to '#{errMessage}'"
 	logger.log({ message, isSystem: true })
-	utils.mixpanelTrack(logType.eventName, {app, err})
+	utils.mixpanelTrack(logType.eventName, {app, error})
 	return

 kill = (app) ->


Fix instrumentor due to jsdom version up

--- src/instrumentor.coffee
+++ src/instrumentor.coffee
@@ -39,12 +39,12 @@
       return '[Anonymous]'

   @instrumentFunctionTraceDefinition2Page: (page, profile, tracer) ->
-    window   = jsdom.jsdom(page.code).parentWindow
-    doc      = window.document
-    scriptEl = doc.createElement("script")
+    doc                = jsdom.jsdom(page.code)
+    window             = doc.defaultView
+    scriptEl           = window.document.createElement("script")
     scriptEl.innerHTML = tracer.generateTraceDefinition page.id, profile.id
-    doc.head.insertBefore scriptEl, doc.head.firstChild
-    window.document.innerHTML
+    window.document.head.insertBefore scriptEl, window.document.head.firstChild
+    doc.documentElement.outerHTML
   @instrumentFunctionTraceDefinition: (source, tracer) ->
     source.code = tracer.generateTraceDefinition() + source.code
   @instrumentFunctionTrace: (source, tracer) ->
@@ -107,6 +107,11 @@
           type: S.BlockStatement,
           body: [traceNode, node]
       }
+      when S.ForInStatement
+        parent.body = {
+          type: S.BlockStatement,
+          body: [traceNode, node]
+      }
       else
-        throw new Error('unexpected return statement')
+        throw new Error('unexpected return statement:' + parent.type)
 module.exports = Instrumentor


update DHL client to use non-secure connections

--- src/dhl.coffee
+++ src/dhl.coffee
@@ -146,7 +146,7 @@

   requestOptions: ({trackingNumber}) ->
     method: 'POST'
-    uri: 'https://eCommerce.Airborne.com/APILanding.asp'
+    uri: 'http://eCommerce.Airborne.com/APILanding.asp'
     body: @generateRequest trackingNumber

 module.exports = {DhlClient}


making string literals actually work

making string literals actually work

--- src/runtime.coffee
+++ src/runtime.coffee
@@ -25,7 +25,15 @@
   constructor: (class_data, @print, initial_args) ->
     @classes = {}
     @classes[class_data.this_class] = class_data
-    @heap = []
+    @heap = {}
+    # add the string references to the heap, in a semi-hacky way
+    cp = class_data.constant_pool
+    str_refs = []
+    cp.each (i,c) -> str_refs.push c.value if c.type is 'String'
+    # make sure the strings get allocated after their refs
+    @high_heap_ref = if str_refs.length > 0 then _.max(str_refs) else 0
+    for sr in str_refs
+      @heap[sr] = @heap[@init_string(cp.get(sr).value)]
     args = @init_array('java/lang/String',(@init_string(a) for a in initial_args))
     @meta_stack = [new root.StackFrame(['fake','frame'],[args])]

@@ -52,10 +60,12 @@

   # heap manipulation
   get_obj: (oref) ->
-    obj = @heap[oref]
-    obj = @string_pool[oref] unless obj #megahack
-    throw "undefined heap/string reference: #{oref}" unless obj
-    obj
+    throw "undefined heap reference: #{oref}" unless @heap[oref]
+    @heap[oref]
+  set_obj: (obj) ->
+    @heap[++@high_heap_ref] = obj
+    @high_heap_ref
+
   heap_new: (cls) -> @push @init_object(cls)
   heap_put: (field_spec) ->
     val = if field_spec.sig.type in ['J','D'] then @pop2() else @pop()
@@ -77,21 +87,15 @@
     console.log field.static_value
     @push field.static_value

+  # heap object initialization
   init_object: (cls) ->
     @class_lookup cls
-    @heap.push {'type':cls}
-    @heap.length - 1
-
-  init_array: (type,arr=[]) ->
-    @heap.push {'type':"[#{type}]", 'array':arr}
-    @heap.length - 1
-
+    @set_obj {'type':cls}
+  init_array: (type,arr=[]) -> @set_obj {'type':"[#{type}]", 'array':arr}
   init_string: (str) ->
     #TODO: actually make a real string object here (as in `new String(str);`)
     c_ref = @init_array('char',arr=(str.charCodeAt(i) for i in [0...str.length]))
-    @heap.push {'type':'java/lang/String', 'value':c_ref, 'count':str.length}
-    @heap.length - 1
-
+    @set_obj {'type':'java/lang/String', 'value':c_ref, 'count':str.length}
   init_field: (field) ->
     if field.type.type is 'reference' and field.type.ref_type is 'class'
       @init_object field.type.referent.class_name
@@ -102,6 +106,7 @@
     else
       throw "I don't know what to do with non-class static fields"

+  # lookup methods
   class_lookup: (cls) ->
     unless @classes[cls]
       # fetch the relevant class file, make a ClassFile, put it in @classes[cls]
@@ -109,7 +114,6 @@
       @classes[cls] = load_external cls
     throw "class #{cls} not found!" unless @classes[cls]
     @classes[cls]
-
   method_lookup: (method_spec) ->
     c = @class_lookup(method_spec.class)
     while true
@@ -121,7 +125,6 @@
     throw "no such method found: #{method_spec.sig.name}" unless ms[0]
     console.log c['this_class']
     ms[0]
-
   field_lookup: (field_spec) ->
     c = @class_lookup(field_spec.class)
     while true


:fire: .only

:fire: .only

--- test/markov.test.coffee
+++ test/markov.test.coffee
@@ -15,7 +15,7 @@
   describe 'with default settings', ->
     beforeEach (done) -> room.robot.configure {}, done

-    it.only 'stores text in the default model', (done) ->
+    it 'stores text in the default model', (done) ->
       room.user.say('me', 'aaa bbb ccc')
       .then ->
         expect(room.messages).to.deep.equal [


npm test _really_ ought to run against src.

I just tried developing a new feature and UGH

Maybe we ultimately make this configurable or
something to test both paths.

--- test/node-helper.coffee
+++ test/node-helper.coffee
@@ -1,4 +1,4 @@
-global.testdouble = require('./../lib/testdouble')
+global.testdouble = require('./../src/testdouble')
 global._ = require('lodash')
 global.chai = require('chai')


fixed tests

fixed tests

--- src/model/statsd.coffee
+++ src/model/statsd.coffee
@@ -34,7 +34,8 @@
   var filtersObject = this.request.query;
   var userRequesting = this.authenticated,
       path = '',
-      results = {};
+      results = {},
+      final = {};
   var allowedIds = allowedIds.length > 0 ? allowedIds : yield metrics.getAllowedChannelIDs(userRequesting)
   var data = []
   var status_array = ['Processing', 'Failed', 'Completed', 'Successful', 'Completed with error(s)']
@@ -45,25 +46,27 @@
         path = render_url + ".Statuses." + status_array[i] + ".count,'1week'))&from=-1days&format=json";
         console.log(path);
         results[status_array[i]] = yield exports.fetchData(path);
-        console.log(results)
+        final[status_array[i]] = 'data' in results[status_array[i]] ? results[status_array[i]].data[0][0] + results[status_array[i]].data[1][0] : 0,
+
+        console.log(final)
       };
-  }

-  var failed = 'data' in results.Failed ? results.Failed.data[0][0] + results.Failed.data[1][0] : 0,
-      processing = 'data'  in results.Processing ? results.Processing.data[0][0] + results.Processing.data[1][0] : 0,
-      completed = 'data' in results.Completed ? results.Completed.data[0][0] + results.Completed.data[1][0] : 0,
-      successful = 'data' in  results.Successful ? results.Successful.data[0][0] + results.Successful.data[1][0] : 0,
-      completedWErrors = 'data' in results['Completed with error(s)'] ? results['Completed with error(s)'].data[0][0] + results['Completed with error(s)'].data[1][0] : 0;
+      //var failed = 'data' in results.Failed ? results.Failed.data[0][0] + results.Failed.data[1][0] : 0,
+      //    processing = 'data'  in results.Processing ? results.Processing.data[0][0] + results.Processing.data[1][0] : 0,
+      //    completed = 'data' in results.Completed ? results.Completed.data[0][0] + results.Completed.data[1][0] : 0,
+      //    successful = 'data' in  results.Successful ? results.Successful.data[0][0] + results.Successful.data[1][0] : 0,
+      //    completedWErrors = 'data' in results['Completed with error(s)'] ? results['Completed with error(s)'].data[0][0] + results['Completed with error(s)'].data[1][0] : 0;

-  data.push({
-    _id : {"channelID": allowedIds[j] },
-    failed: failed,
-    successful: successful,
-    processing:processing,
-    completed:completed,
-    completedWErrors:completedWErrors
-  });
+    data.push({
+      _id : {"channelID": allowedIds[j] },
+      failed: final.Failed,
+      successful: final.Successful,
+      processing:final.Processing,
+      completed:final.Completed,
+      completedWErrors:final['Completed with error(s)']
+    });
+  }

   this.body = data
 }`


Use path.sep instead of path.seq

Use path.sep instead of path.seq

--- src/git.coffee
+++ src/git.coffee
@@ -122,7 +122,7 @@
   if process.platform is 'win32'
     /^[a-zA-Z]+:[\\\/]$/.test(repositoryPath)
   else
-    repositoryPath is path.seq
+    repositoryPath is path.sep

 exports.open = (repositoryPath) ->
   symlink = realpath(repositoryPath) isnt repositoryPath


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera_fade.coffee
+++ src/camera_fade.coffee
@@ -10,7 +10,6 @@
 ###
 Camera.Fade = (I, self) ->
   Object.reverseMerge I,
-
     flashTargetAlpha: 0

   fadeInDefaults =


isFunction

isFunction

--- src/god.coffee
+++ src/god.coffee
@@ -1,6 +1,9 @@
 # Data model
+isFunction = (value) ->
+  typeof(value) == "function"
+
 normalize_value = (rx, value) ->
-  if typeof(value) == "function"
+  if isFunction(value)
     value
   else if value instanceof Object
     value = new World(rx, value)
@@ -19,6 +22,10 @@
   get: (key) ->
     @doc.get(key)

+  get_value: (key) ->
+    value = @doc.get(key)
+    value
+
   put: (key, value) ->
     @doc.put(key, value)


seen is a Date object, not timestamp

seen is a Date object, not timestamp

--- src/collections.coffee
+++ src/collections.coffee
@@ -8,8 +8,8 @@
     return 1
   unless b.seen
     return -1
-  aSeen = if typeof a.seen is 'object' then a.seen else new Date a.seen
-  bSeen = if typeof b.seen is 'object' then b.seen else new Date b.seen
+  a.seen = if typeof a.seen is 'object' then a.seen else new Date a.seen
+  b.seen = if typeof b.seen is 'object' then b.seen else new Date b.seen
   if a.seen > b.seen
     return -1
   if b.seen > a.seen


Get enabling/disabling of scrolling actually working

--- src/index.cjsx
+++ src/index.cjsx
@@ -43,15 +43,17 @@
   componentDidMount: ->
     @setState height: @getDOMNode().offsetHeight
     unless @props.disable
-      @eventListener = window.addEventListener('scroll', @handleScroll, false)
+      window.addEventListener('scroll', @handleScroll)

   componentWillReceiveProps: (nextProps) ->
-    if nextProps.disable
+    if nextProps.disable and not @props.disable
       @unfix()

-      # Remove the scroll listener if there is one.
-      if @eventListener
-        window.removeEventListener('scroll', @handleScroll)
+      # Remove the event listener
+      window.removeEventListener('scroll', @handleScroll)
+
+    else if not nextProps.disable and @props.disable
+      window.addEventListener('scroll', @handleScroll)

   handleScroll: ->


allow module to export an array of rules

--- src/commandline.coffee
+++ src/commandline.coffee
@@ -277,7 +277,7 @@
             coffeelint.registerRule ruleModule, ruleName
         else
             # Or it can export an array of rules to load.
-            for rule of ruleModule
+            for rule in ruleModule
                 coffeelint.registerRule rule
     catch e
         console.error "Error loading #{moduleName}"


Adding some helpful errors for missing environment variables

--- src/irc.coffee
+++ src/irc.coffee
@@ -73,8 +73,18 @@
   command: (command, strings...) ->
     @bot.send command, strings...

+  checkCanStart: ->
+    if not process.env.HUBOT_IRC_NICK or @robot.name
+      throw new Error("HUBOT_IRC_NICK is not defined; try: export HUBOT_IRC_NICK='mybot'")
+    else if not process.env.HUBOT_IRC_ROOMS
+      throw new Error("HUBOT_IRC_ROOMS is not defined; try: export HUBOT_IRC_ROOMS='#myroom'")
+    else if not process.env.HUBOT_IRC_SERVER
+      throw new Error("HUBOT_IRC_SERVER is not defined: try: export HUBOT_IRC_SERVER='irc.myserver.com'")
+
   run: ->
     self = @
+
+    do @checkCanStart

     options =
       nick:     process.env.HUBOT_IRC_NICK or @robot.name


expiration mechanism

expiration mechanism

--- src/uapanfw.coffee
+++ src/uapanfw.coffee
@@ -23,20 +23,58 @@
 modulename = 'fw'
 blacklistfile = modulename + ".json"
 displayfmt = '%-4s %-50s %-15s %s'
-
-isAuthorized = (robot, msg) ->
+svcQueueIntervalMs = 300 * 1000
+
+robotRef = false
+
+isAuthorized = (msg) ->
+  console.error 'bad robotRef' unless robotRef
   u = msg.envelope.user
-  return true if robot.auth.hasRole(u,'fw')
+  return true if robotRef.auth.hasRole(u,'fw')
   msg.reply "Not authorized.  Missing fw role."
   return false

-is2fa = (robot, msg) ->
+is2fa = (msg) ->
+  console.error 'bad robotRef' unless robotRef
   u = msg.envelope.user
-  return true if robot.auth.is2fa(u)
+  return true if robotRef.auth.is2fa(u)
   msg.reply "2fa required.  Use `auth 2fa` to validate identity."
   return false

+expirationWorker = ->
+  bl = fwdata.blacklist
+  removequeue = []
+  deleted = [
+    sprintf displayfmt, 'Type', 'Value', 'Expiration', 'Creator'
+  ]
+  for obj in fwdata.blacklist when moment(obj.expires).isAfter()
+    deleted.push sprintf displayfmt, obj.type, obj.val, expires.fromNow(),
+      obj.creator
+    removequeue.push obj
+
+  while removequeue.length > 0
+    obj = removequeue.shift()
+    bl.splice(bl.indexOf(obj), 1)
+
+  msg = "fw: blacklist entries expired and have been removed. " +
+    "Change will be applied in < 5 minutes. Removed: " +
+    "```"+ deleted.join("\n") + "```"
+  notifySubscribers msg
+
+  fs.writeFileSync blacklistfile, JSON.stringify(fwdata), 'utf-8'
+
+  setTimeout expirationWorker, svcQueueIntervalMs
+
+notifySubscribers = (msg, current_un = false) ->
+  console.error 'bad robotRef' unless robotRef
+  for un in fwdata.notify
+    robotRef.send { room: un }, msg unless current_un && un == current_un
+
+
 module.exports = (robot) ->
+
+  robotRef = robot
+  setTimeout expirationWorker, svcQueueIntervalMs

   fwdata =
     notify: []
@@ -152,8 +190,8 @@
     return

   robot.respond /fw (?:blacklist|b) add (url|cidr) ([^ ]+)(?: ([^ ]+)|)$/i, (msg) ->
-    return unless isAuthorized robot, msg
-    return unless is2fa robot, msg
+    return unless isAuthorized msg
+    return unless is2fa msg

     who = msg.envelope.user.name

@@ -199,16 +237,15 @@
       "added entry to blacklist"
     robot.logger.info logmsg

-    for un in fwdata.notify
-      usermsg = usermsg.replace(/^You/, un)
-      #usermsg = usermsg.replace(/  Change will be applied in \< 5 minutes\./, '')
-      robot.send { room: un }, usermsg unless un == who
+    usermsg = usermsg.replace(/^You/, un)
+    #usermsg = usermsg.replace(/  Change will be applied in \< 5 minutes\./, '')
+    notifySubscribers usermsg, who

     return

   robot.respond /fw (?:blacklist|b) (?:delete|del|d) (url|cidr) ([^ ]+)$/i, (msg) ->
-    return unless isAuthorized robot, msg
-    return unless is2fa robot, msg
+    return unless isAuthorized msg
+    return unless is2fa msg

     who = msg.envelope.user.name
     bl_type = String(msg.match[1])
@@ -232,16 +269,16 @@
       expires = moment(obj.expires)
       if bl_type and bl_type != obj.type
         errfmt = 'type: [%s] != [%s]'
-        console.log sprintf errfmt, bl_type, obj.type
+        #console.log sprintf errfmt, bl_type, obj.type
         new_bl.push obj
         continue
       if bl_search and obj.val.indexOf(bl_search) == -1
         errfmt = 'search: indexOf[%s] not found in [%s]'
-        console.log sprintf errfmt, bl_search, obj.val
+        #console.log sprintf errfmt, bl_search, obj.val
         new_bl.push obj
         continue
       if expires.isBefore() # now
-        console.log 'expires: ['+ expires +'] is before now'
+        #console.log 'expires: ['+ expires +'] is before now'
         new_bl.push obj
         continue
       deleted.push sprintf displayfmt, obj.type, obj.val,
@@ -262,10 +299,10 @@
       "removed #{deltaN} entries from blacklist"
     robot.logger.info logmsg

-    for un in fwdata.notify
+    if deltaN > 0
       usermsg = usermsg.replace(/^You/,un)
       #usermsg = usermsg.replace(/  Change will be applied in \< 5 minutes\./, '')
-      robot.send { room: un }, usermsg unless un == who or deltaN < 1
+      notifySubscribers usermsg, who

     return


make add blacklist terse after first request

--- src/uapanfw.coffee
+++ src/uapanfw.coffee
@@ -52,6 +52,7 @@
   blacklist: []
   whitelist: []
   firewalls: []
+  terse: []

 fwnames =
   '10.9.0.252': 'Fairbanks-1'
@@ -227,12 +228,12 @@

   expires = String(msg.match.shift())
   if expires isnt 'undefined'
-    extra = expires.match /\+(\d+)([a-zA-Z])/
+    extra = expires.match /\+(\d+)([a-zA-Z]+)/
     if extra?
       n = extra[1]
       unit = extra[2]
-      unless unit in ['h','d','w','M','Q','y']
-        usermsg = "Invalid unit `#{unit}` in expiration `#{expires}`. Use h for hours, d for days, w for weeks, M for months, Q for quarters, or y for years."
+      unless unit in ['h','hours','d','days','w','weeks','M','months','Q','quarters','y','years']
+        usermsg = "Invalid unit `#{unit}` in expiration `#{expires}`. Use h or hours, d or days, w or weeks, M or months, Q or quarters, y or years."
         return msg.reply usermsg
       entry.expires = moment().add(n,unit).format()
     else if moment(expires).isValid()
@@ -248,16 +249,24 @@
   fwdata[list_name].push entry
   writeData()

-  usermsg = "#{who} added `#{entry.val}` to firewall #{list_name}. " +
-    "Expires `#{entry.expires}`.  Change will be applied in < 5 minutes."
-  msg.reply usermsg
+  usermsg = "Added `#{entry.val}` to fw #{list_name}."
+  if expires != 'undefined'
+    usermsg += "  Expires `#{entry.expires}`."
+  unless fwdata.terse[who] && fwdata.terse[who].isAfter()
+    usermsg += "  Change will be applied in < 5 minutes."
+  msg.send usermsg

   logmsg = "#{modulename}: robot responded to #{who}: " +
     "added entry to #{list_name}"
   robot.logger.info logmsg

-  #usermsg = usermsg.replace(/  Change will be applied in \< 5 minutes\./, '')
-  notifySubscribers usermsg, who
+  notifymsg = "#{who} added `#{entry.val}` to fw #{list_name}."
+  if expires isnt 'undefined'
+    notifymsg += "  Expires `#{entry.expires}`."
+  notifySubscribers notifymsg, who
+
+  # be terse after the first utterance
+  fwdata.terse[who] = moment().add(30,'minutes')

 extendListEntry = (robot, msg) ->


All shapes take a single dict of args with defaults (fix #128)

--- src/core/shapes.coffee
+++ src/core/shapes.coffee
@@ -80,7 +80,10 @@

 defineShape 'Image',
   # TODO: allow resizing/filling
-  constructor: ({@x, @y, @image}) ->
+  constructor: (args={}) ->
+    @x = args.x or 0
+    @y = args.y or 0
+    @image = args.image or null
   draw: (ctx, retryCallback) ->
     if @image.width
       ctx.drawImage(@image, @x, @y)
@@ -94,10 +97,14 @@

 defineShape 'Rectangle',
-  constructor: (
-      {@x, @y, @strokeWidth, @strokeColor, @fillColor, @width, @height}) ->
-    @width ?= 0
-    @height ?= 0
+  constructor: (args={}) ->
+    @x = args.x or 0
+    @y = args.y or 0
+    @width = args.width or 0
+    @height = args.height or 0
+    @strokeWidth = args.strokeWidth or 1
+    @strokeColor = args.strokeColor or 'black'
+    @fillColor = args.fillColor or 'transparent'

   draw: (ctx) ->
     ctx.fillStyle = @fillColor
@@ -111,7 +118,13 @@

 defineShape 'Line',
-  constructor: ({@x1, @y1, @x2, @y2, @strokeWidth, @color}) ->
+  constructor: (args={}) ->
+    @x1 = args.x1 or 0
+    @y1 = args.y1 or 0
+    @x2 = args.x2 or 0
+    @y2 = args.y2 or 0
+    @strokeWidth = args.strokeWidth or 1
+    @color = args.color or 'black'

   draw: (ctx) ->
     ctx.lineWidth = @strokeWidth
@@ -127,7 +140,11 @@

 linePathFuncs =
-  constructor: (_points = [], @order = 3, @tailSize = 3) ->
+  constructor: (args={}) ->
+    points = args.points or []
+    @order = args.order or 3
+    @tailSize = args.tailSize or 3
+
     # The number of smoothed points generated for each point added
     @segmentSize = Math.pow(2, @order)

@@ -135,7 +152,7 @@
     @sampleSize = @tailSize + 1

     @points = []
-    for point in _points
+    for point in points
       @addPoint(point)

   toJSON: ->
@@ -145,7 +162,8 @@
   fromJSON: (data) ->
     points = (JSONToShape(pointData) for pointData in data.points)
     return null unless points[0]
-    createShape('LinePath', points, data.order, data.tailSize)
+    createShape(
+      'LinePath', {points, order: data.order, tailSize: data.tailSize})

   draw: (ctx) ->
     @drawPoints(ctx, @smoothedPoints)
@@ -228,20 +246,29 @@
   fromJSON: (data) ->
     points = (JSONToShape(pointData) for pointData in data.points)
     return null unless points[0]
-    createShape('ErasedLinePath', points, data.order, data.tailSize)
+    createShape(
+      'ErasedLinePath', {points, order: data.order, tailSize: data.tailSize})

 defineShape 'Point',
-  constructor: ({@x, @y, @size, @color}) ->
+  constructor: (args={}) ->
+    @x = args.x or 0
+    @y = args.y or 0
+    @size = args.size or 0
+    @color = args.color or ''
   lastPoint: -> this
-  draw: (ctx) -> console.log 'draw point', @x, @y, @size, @color
+  draw: (ctx) -> throw "not implemented"
   toJSON: -> {@x, @y, @size, @color}
   fromJSON: (data) -> createShape('Point', data)

 defineShape 'Text',
-  constructor: ({@x, @y, @text, @color, @font}) ->
-    @font ?= '18px sans-serif'
+  constructor: (args={}) ->
+    @x = args.x or 0
+    @y = args.y or 0
+    @text = args.text or ''
+    @color = args.color or 'black'
+    @font  = args.font or '18px sans-serif'
   draw: (ctx) ->
     ctx.font  = @font
     ctx.fillStyle = @color


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/core_object.coffee
+++ src/core_object.coffee
@@ -147,6 +147,7 @@
           Module = Module.constantize()

         else
+
         self.extend Module(I, self)

       return self


Handle different status of course registration

--- src/course/status.cjsx
+++ src/course/status.cjsx
@@ -20,11 +20,14 @@
     course.set(ecosystem_book_uuid: @props.collectionUUID)
     course.register(React.findDOMNode(@refs.input).value)

+  startConfirmation: ->
+    @getCourse().confirm()
+
   onCourseChange: -> @forceUpdate()
   componentWillUnmount: -> @course.channel.off('change', @onCourseChange) if @course
   getCourse: ->
     return @course if @course
-    @course = @getUser().getCourse(@props.collectionUUID) or new Course()
+    @course = @getUser().findOrCreateCourse(@props.collectionUUID)
     @course.channel.on('change', @onCourseChange)
     @course

@@ -38,8 +41,17 @@

{errors}

   render: ->
-    if @getUser().getCourse(@props.collectionUUID)
-      You have joined this course, start working now!
+    course = @getUser().getCourse(@props.collectionUUID)
+    if course
+      if course.isIncomplete()
+        Joining ...
+      else if course.isPending()
+
+          Would you like to join {course.description()}?
+          <button onClick={@startConfirmation}>Confirm</button>
+
+      else
+        You have joined {course.description()}, start working now!
     else

         Register for course using invite code:


Simplified popup highlight handling.

Simplified popup highlight handling.

--- src/map.coffee
+++ src/map.coffee
@@ -29,7 +29,7 @@
                 unless @units.isEmpty()
                     @refit_bounds()
             @listenTo @selected_units, 'reset', (units, options) ->
-                @clear_popups()
+                @clear_popups(true)
                 if units.isEmpty()
                     return
                 unit = units.first()
@@ -72,12 +72,19 @@
         to_coordinates: (windowCoordinates) ->
             @map.layerPointToLatLng(@map.containerPointToLayerPoint(windowCoordinates))

-        clear_popups: ->
-            @popups.clearLayers()
+        clear_popups: (clear_selected) ->
+            @popups.eachLayer (layer) =>
+                if clear_selected
+                    layer.selected = false
+                    @popups.removeLayer layer
+                else unless layer.selected
+                    @popups.removeLayer layer

         remove_units: (options) ->
             @all_markers.clearLayers()
             @draw_units @units
+            unless @selected_units.isEmpty()
+                @highlight_selected_unit @selected_units.first()

         remove_unit: (unit, units, options) ->
             if unit.marker?
@@ -139,8 +146,9 @@
             # Prominently highlight the marker whose details are being
             # examined by the user.
             marker = unit.marker
-            @clear_popups()
+            @clear_popups(true)
             popup = marker.getPopup()
+            popup.selected = true
             popup.setLatLng marker.getLatLng()
             @popups.addLayer popup
             $(marker?._popup._wrapper).addClass 'selected'
@@ -148,10 +156,12 @@
         highlight_unselected_unit: (unit) ->
             # Transiently highlight the unit which is being moused
             # over in search results or otherwise temporarily in focus.
+            marker = unit.marker
+            popup = marker?.getPopup()
+            if popup?.selected
+                return
             @clear_popups()
             parent = @all_markers.getVisibleParent unit.marker
-            marker = unit.marker
-            popup = marker?.getPopup()
             if popup?
                 $(marker._popup._wrapper).removeClass 'selected'
                 popup.setLatLng marker?.getLatLng()