Building chat applications and robots for Skype

Command line is powerful interface. Everyone who has taken head first dive into UNIX and survived back to tell about it can confirm. However, you do not need to be a computer guru to interact with command line. In this blog post, I’ll want to open you the window of opportunity to realize some potential here in (Skype) chats. Please note that this blog post is not limited to the scope of Skype, but principles presented here can be applied to any chat media your team is using.

sevabot-256

1. Why build a chat robot?

Generally, non-IT-sawy folks interact with few command-line-likes in their daily work

  • Browser address bar (Firefox’s Awesome Bar has made awesome progress to shift my cognitive load into the browser address bar)
  • Search engine’s search box (autocompletion, but DuckDuckGo offers even more awesome tricks here)
  • Microsoft Excel input box, or similar
  • Chat and instant messaging message inputs
  • etc.

Command line is powerful, as stated before. It’s fast to type, but it’s also more error prone and more difficult to discover, as opposite to e.g. menu-driven interface or a web application. Clickety-click encourages discoverability by showing you the options as you progress. Command line is more like you need to know what you type and you do it as single pass – if there are errors you restart the process from the start. For this very reason command line is more effective for repeating tasks: typing has no speed limit and comes from your muscle memory. After you get past the command line learning curve it’ll be all harder, better, faster, stronger.

2. Skype chat, universal command line

Here is a real-life example: a managerial person in a small organization wanted to have a better picture what all of their team members are doing. Due to nature the of the work, the persons are not present at the office: they work here and there in client premises.

We could use a nice web app or even a mobile app to track the task everybody is working on. However, this particular manager thought “It’s too heavy weight.” The business case was that when he gets a call from a customer asking whether this task is done or started yet, he needs to know it at the moment. The organization is small, everybody is a geek and doing Bring Your Own Devices, so the app should be reachable on whatever you have in hand.

The manager had seen some the earlier automation efforts we had made with a Skype chat bot, Sevabot. So he asked me if we could build a Skype-chat-driven keep-it-simple task list application: just to mark whatever you are working on and mark again when you are done, all this by Skype chat commands.

Benefits of this approach

  • Everybody in the team was already using Skype: people are familiar with it
  • Skype is notoriously famous for working on every device, every network, everywhere
  • Skype syncs: even in offline you have some status messages left on your mobile device
  • Command line is efficient: Writing a chat message to output ongoing tasks can be done even when you are on mobile call. It literally takes a second.
  • Writing a simple command-driven application has very good cost-benefit ratio: writing a text based applications is a quick task, can be done in couple of hours (as opposite to build a web app, some forms, etc.)

And this is how it works:

Screen Shot 2013-03-28 at 1.47.40 PM

3. Creating a stateful chat app with Sevabot

Sevabot is an on-going open source Skype chat bot project. Before (version 1.2) it already had support for running UNIX scripts through chat commands and HTTP webhooks to send chat notifications from external services. The task in hand could have been created with little bit UNIX scripting magic. However, I had been in touch with Naoto Yokoyama who had a need for more advanced Skype chat commands besides simple triggers. Based on the discussion and Naoto’s original work we created a stateful chat scripting support for Sevabot.

  • You define your (stateful) chat application as a simple Python module and class
  • The modules are reloadable
  • Installing a new stateful script is simple as dropping the .py file into the correct folder
  • The script can install event handlers for all raw Skype4Py events, including handling of Skype calls
  • You can utilize background processes, timers, threads, native extensions and all the power of normal long-running Python UNIX apps

Below is the Python source for the task management application described above.

Some notes about the module

  • We persistent the script state to the disk. Sevabot is running in the cloud and it can be restarted at any moment. We simply use Python pickles, no databases needed here.
  • Each group chat has its own task list, so it is safe to multiplex one bot instance across different teams and working groups.
  • There is a timer which will poke the people if they forgot to close the tasks they have started.

4. Future

Next, I’d like to try the following

5. Source code

The module source code is also on Github and distributed with Sevabot 1.2.

#!/sevabot
"""

    Simple group chat task manager.

    This also serves as an example how to write stateful handlers.

"""

from __future__ import unicode_literals

from threading import Timer
from datetime import datetime
import os
import logging
import pickle
from collections import OrderedDict

from sevabot.bot.stateful import StatefulSkypeHandler
from sevabot.utils import ensure_unicode, get_chat_id

logger = logging.getLogger("Tasks")

# Set to debug only during dev
logger.setLevel(logging.INFO)

logger.debug("Tasks module level load import")

# How long one can work on a task before we give a warning
MAX_TASK_DURATION = 24*60*60

HELP_TEXT = """!tasks is a noteboard where virtual team members can share info which tasks they are currently working on.

Commands
------------------------------

!tasks: This help text

Start task: You start working on a task. When you started is recorded. Example:

    start task I am now working on new Sevabot module interface

Stop task: Stop working on the current task. Example:

    stop task

List tasks: List all tasks an people working on them. Example:

    list tasks

Task lists are chat specific and the list is secure to the members of the chat.
All commands are case-insensitive.
"""

class TasksHandler(StatefulSkypeHandler):
    """
    Skype message handler class for the task manager.
    """

    def __init__(self):
        """Use `init` method to initialize a handler.
        """
        logger.debug("Tasks constructed")

    def init(self, sevabot):
        """
        Set-up our state. This is called

        :param skype: Handle to Skype4Py instance
        """
        logger.debug("Tasks init")
        self.sevabot = sevabot
        self.status_file = os.path.join(os.path.dirname(__file__), "sevabot-tasks.tmp")
        self.status = Status.read(self.status_file)

        self.commands = {
            "!tasks": self.help,
            "start task": self.start_task,
            "list tasks": self.list_tasks,
            "stop task": self.stop_task,
        }

        self.reset_timeout_notifier()

    def handle_message(self, msg, status):
        """Override this method to customize a handler.
        """

        # Skype API may give different encodings
        # on different platforms
        body = ensure_unicode(msg.Body)

        logger.debug("Tasks handler got: %s" % body)

        # Parse the chat message to commanding part and arguments
        words = body.split(" ")
        lower = body.lower()

        if len(words) == 0:
            return False

        # Parse argument for two part command names
        if len(words) >= 2:
            desc = " ".join(words[2:])
        else:
            desc = None

        chat_id = get_chat_id(msg.Chat)

        # Check if we match any of our commands
        for name, cmd in self.commands.items():
            if lower.startswith(name):
                cmd(msg, status, desc, chat_id)
                return True

        return False

    def shutdown(self):
        """ Called when the module is reloaded.
        """
        logger.debug("Tasks shutdown")
        self.stop_timeout_notifier()

    def save(self):
        """
        Persistent our state.
        """
        Status.write(self.status_file, self.status)

    def reset_timeout_notifier(self):
        """
        Check every minute if there are overdue jobs
        """
        self.notifier = Timer(60.0, self.check_overdue_jobs)
        self.notifier.daemon = True  # Make sure CTRL+C works and does not leave timer blocking it
        self.notifier.start()

    def stop_timeout_notifier(self):
        """
        """
        self.notifier.cancel()

    def help(self, msg, status, desc, chat_id):
        """
        Print help text to chat.
        """

        # Make sure we don't trigger ourselves with the help text
        if not desc:
            msg.Chat.SendMessage(HELP_TEXT)

    def warn_overdue(self, chat_id, job):
        """
        Generate overdue warning.
        """
        self.sevabot.sendMessage(chat_id, "Task hanging: %s started working on %s, %s" % (job.real_name, job.desc, pretty_time_delta(job.started)))
        job.warned = True

    def check_overdue_jobs(self):
        """
        Timer callback to go through jobs which might be not going forward.
        """

        found = False

        logger.debug("Running overdue check")

        now = datetime.now()

        for chat_id, chat in self.status.chats.items():
            for job in chat.values():
                if (now - job.started).total_seconds() > MAX_TASK_DURATION and not job.warned:
                    found = True
                    self.warn_overdue(chat_id, job)

        if found:
            logger.debug("Found overdue jobs")
            self.save()
        else:
            logger.debug("Did not found overdue jobs")

        # http://www.youtube.com/watch?v=ZEQydmaPjF0
        self.reset_timeout_notifier()

    def start_task(self, msg, status, desc, chat_id):
        """
        Command handler.
        """

        if desc.strip() == "":
            msg.Chat.SendMessage("Please give task description also")
            return

        tasks = self.status.get_tasks(chat_id)
        existing_job = tasks.get(msg.Sender.Handle, None)
        if existing_job:
            msg.Chat.SendMessage("Stopped existing task %s" % existing_job.desc)

        job = Job(msg.Sender.FullName, datetime.now(), desc)
        tasks = self.status.get_tasks(chat_id)
        tasks[msg.Sender.Handle] = job
        self.save()
        msg.Chat.SendMessage("%s started working on %s." % (job.real_name, job.desc))

    def list_tasks(self, msg, status, desc, chat_id):
        """
        Command handler.
        """

        jobs = self.status.get_tasks(chat_id).values()

        if len(jobs) == 0:
            msg.Chat.SendMessage("No active tasks for anybody")

        for job in jobs:
            msg.Chat.SendMessage("%s started working on %s, %s" % (job.real_name, job.desc, pretty_time_delta(job.started)))

    def stop_task(self, msg, status, desc, chat_id):
        """
        Command handler.
        """
        tasks = self.status.get_tasks(chat_id)
        if msg.Sender.Handle in tasks:
            job = tasks[msg.Sender.Handle]
            del tasks[msg.Sender.Handle]
            msg.Chat.SendMessage("%s finished" % job.desc)
        else:
            msg.Chat.SendMessage("%s had no active task" % msg.Sender.FullName)

        self.save()

class Status:
    """
    Stored pickled state of the tasks.

    Use Python pickling serialization for making status info persistent.
    """

    def __init__(self):
        # Chat id -> OrderedDict() of jobs mappings
        self.chats = dict()

    @classmethod
    def read(cls, path):
        """
        Read status file.

        Return fresh status if file does not exist.
        """

        if not os.path.exists(path):
            # Status file do not exist, get default status
            return Status()

        f = open(path, "rb")

        try:
            return pickle.load(f)
        finally:
            f.close()

    @classmethod
    def write(cls, path, status):
        """
        Write status file
        """
        f = open(path, "wb")
        pickle.dump(status, f)
        f.close()

    def get_tasks(self, chat_id):
        """
        Get jobs of a particular chat.
        """
        if not chat_id in self.chats:
            # Skype username -> Task instance mappings
            self.chats[chat_id] = OrderedDict()

        return self.chats[chat_id]

class Job:
    """
    Tracks who is doing what
    """

    def __init__(self, real_name, started, desc):
        """
        :param started: datetime when the job was started
        """
        self.started = started
        self.desc = desc
        self.real_name = real_name
        # Have we given timeout warning for this job
        self.warned = False

# The following has been
# ripped off from https://github.com/imtapps/django-pretty-times/blob/master/pretty_times/pretty.py

_ = lambda x: x

def pretty_time_delta(time):

    now = datetime.now(time.tzinfo)

    if time > now:
        past = False
        diff = time - now
    else:
        past = True
        diff = now - time

    days = diff.days

    if days is 0:
        return get_small_increments(diff.seconds, past)
    else:
        return get_large_increments(days, past)

def get_small_increments(seconds, past):
    if seconds < 10:
        result = _('just now')
    elif seconds < 60:
        result = _pretty_format(seconds, 1, _('seconds'), past)
    elif seconds < 120:
        result = past and _('a minute ago') or _('in a minute')
    elif seconds < 3600:
        result = _pretty_format(seconds, 60, _('minutes'), past)
    elif seconds < 7200:
        result = past and _('an hour ago') or _('in an hour')
    else:
        result = _pretty_format(seconds, 3600, _('hours'), past)
    return result

def get_large_increments(days, past):
    if days == 1:
        result = past and _('yesterday') or _('tomorrow')
    elif days < 7:
        result = _pretty_format(days, 1, _('days'), past)
    elif days < 14:
        result = past and _('last week') or _('next week')
    elif days < 31:
        result = _pretty_format(days, 7, _('weeks'), past)
    elif days < 61:
        result = past and _('last month') or _('next month')
    elif days < 365:
        result = _pretty_format(days, 30, _('months'), past)
    elif days < 730:
        result = past and _('last year') or _('next year')
    else:
        result = _pretty_format(days, 365, _('years'), past)
    return result

def _pretty_format(diff_amount, units, text, past):
    pretty_time = (diff_amount + units / 2) / units

    if past:
        base = "%(amount)d %(quantity)s ago"
    else:
        base = "%(amount)d %(quantity)s"

    return base % dict(amount=pretty_time, quantity=text)

# Export the instance to Sevabot
sevabot_handler = TasksHandler()

__all__ = ["sevabot_handler"]

Enjoy.

 

 

 

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

Timeouting commands in shell scripts

Often you want to automatize something using shell scripting. In a perfect world your script robot works for you without getting tired, without hick-ups, and you can just sit at the front of your desk and sip coffee.

Image

Then we enter the real world: Your network is disconnected. DNS goes downs. Your HTTP hooks and downloads stall. Interprocess communication hangs. Effectively this means that even if your script is running correctly from the point of operating system it won’t finish its work before you finish your cup of coffee.

Below is an example how to create timeouts and notifications in a shell script.

1. Never gonna give you up

(c) Rick Astley

Automation must work 100% and you must always know if it doesn’t do that. Otherwise if you cannot trust the automated systems you could have this glorious moment of “oh it has been broken for three months now”. No amount of coffee makes your day after that. Then you spend your nights pondering whether your scripting is running instead of playing Borderlands 2.

Some safety guards around your coffee cup include

  • Sane timeout thresholds for commands to avoid hang situations. E.g. your automated “make smallapp” should probably not run for 50 hours straight.
  • Get a notification if a fully automatized process does not end up as expected. So that you find out the problem right away, not three months later. Do not use email as the communication channel, it is unreliable and has awful signal-to-noise ratio. Better solutions include instant messengers (Skype), SMS (check twilio.com)
  • Get a notification also when a service, which should be running all the time, is restarted.

2. Timeout.bash

The shell scripting (sh, zsh, bash)  does not offer built-in tools for timeouting commands by default (please correct me if I am wrong, but stackoverflow.com et. al folks did not know any better). In Bash cookbook, there exist a timeout wrapper script example (The orignal SO.com answer).

The trick is that when the timeout wrapper terminates the command, you’ll get the exit code of SIGTERM (143) or SIGKILL (137) to the parent script. This might not be entirely clear from reading the script.

3. Using timeout wrapper and sending failure notifications to Skype

Below is an example how you can hook timeout to your own script. Here we have a simple continous integration script (ghetto-ci) which polls version control repositories and on any change executes the test suite. A test failure is reported back to Skype by ghetto-ci using Sevabot Skype bot. The loop script uses a server specific (Ubuntu) way to set up a headless X server, so that the tests can run Firefox on the server (can one call a headless Firefox server “mittens”?) The continous integration loop is deployed simply as leaving it running on the screen‘ed terminal on a server.

Some protection we do: The script has extra checks to protect against hung processes by killing them using pkill before starting each test run. Selenium’s WebDriver seem to often cause situations where the Python tests don’t quit cleanly, leaving around all kind of zombie processes. In this case, the test command timeouts using the timeout wrapper and we get a notification to Skype. Based on this we can refine the test running logic, timeout delay and such to make the script more robust allowing us to focus more on drinking the coffee and less watching a looping process running in a UNIX screen for potential failures.

Pardon me for possibly ugly shell scripting code. SH is not my primary language. The example code is also on Github. Please see the orignal blog post for syntax colored example.

#!/bin/bash
#
# Run CI check for every 5 minutes and
# execute tests if svn has been updated.
#
# The script sets up xvfb (X window framebuffer)
# which is used to run the headless Firefox.
#
# We will post a Skype message if the Selenium WebDriver
# has some issues (it often hangs)
#
# We also signal the tests to use a special static Firefox build
# and do not rely (auto-updated) system Firefox
#
# NOTE: This script is NOT sh compliant (echo -n),
# bash needed

# Timeouting commands shell script helper
# http://stackoverflow.com/a/687994/315168
TIMEOUT=timeout.sh

# Which FF binary we use to run the tests
FIXED_FIREFOX=$HOME/ff16/firefox/firefox

# It's me, Mariooo!
SELF=$(readlink -f "$0")

# Skype endpoint information
SKYPE_CHAT_ID="1234567890"

SKYPE_SHARED_SECRET="toholampi"

SEVABOT_SERVER="http://yourserver.com:5000/msg/"

#
#  Helper function to send Skype messages from help scripts.
#  The messages are signed with a shared seceret.
#
#  Parameter 1 is the message
#
function send_skype_message() {
    msg="$1"
    md5=`echo -n "$SKYPE_CHAT_ID$msg$SKYPE_SHARED_SECRET" | md5sum`

    #md5sum pads a '-' to the end of the string. We need to get rid of that.
    for m in $md5; do
        break
    done

    result=`curl --silent --data-urlencode chat="$SKYPE_CHAT_ID" --data-urlencode msg="$msg" --data-urlencode md5="$m" $SEVABOT_SERVER`
    if [ "$result" != "OK" ] ; then
        echo "Error in HTTP communicating to Sevabot: $result"
    fi
}

# Tell the tests to use downgraded FF!6
# which actually works with Selenium
if [ -e $FIXED_FIREFOX ] ; then
    FIREFOX_PATH=$FIREFOX_PATH
    export FIREFOX_PATH
    echo "Using static Firefox 16 build to run the tests"
fi

send_skype_message "♫ ci-loop.sh restarted at $SELF"

while true
do
    # Kill hung testing processes (it might happen)
    pkill -f "bin/test"

    # Purge existing xvfb just in case
    pkill Xvfb

    sleep 5

    echo "Opening virtual X11"

    # Start headless X
    Xvfb :15 -ac -screen 0 1024x768x24 &

    # Tell FF to use this X server
    export DISPLAY=localhost:15.0

    # Run one cycle of continous integration,
    # give it 15 minutes to finish
    echo "Starting test run"
    $TIMEOUT -t 900 continous-integration.sh
    result=$?

    if [ "$result" == "143" ] ; then
        echo "------- CI TIMEOUT OOPS --------"
        send_skype_message "⚡ Continuous integration tests timed out - check the ci-loop.sh screen for problems"
    fi

    sleep 300
done

Voila. Back to the coffee.

 

 

 

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

Remote presentation screencasts with YouTube recordings for meetups in Google Hangout

After seeing it in happening in a HelsinkiJS, I figured out that having a remote presenter in your local meetup kicks ass. Thanks Oleg for getting our meta meetup together for sharing the best practices!

Why a remote presenter via screencast?

  • Remote presenters offer some more color over seeing the same old faces in your meetup all the time
  • More information trade across organizational borders… or countries.
  • The benefits of the online presentation are not limited for the local participants – anyone can join to see the online presentation online – like all those home daddies who often miss the chance to be there in person.

1. Meet Google Hangout

Google offers Hangout video sharing feature in their Google+ social service. The benefits of doing a screencast presentation in Google Hangout include

  • Anyone can watch the live stream
    • The number of participants is not limited or pre-selectd like e.g. in Skype.
    • You get anonymous Youtube short URL where anyone can follow live broadcast
  • You get automatically YouTube video recording out of your broadcast. This is a big plus as post-processing recordings from the conferences have always been great pain.
  • Google Hangout works on any OS, maybe even on some mobile devices?
  • Google is generously offering the bandwidth from their streaming CDN network. You don’t need to provide the bandwidth for those 100 viewers of 2 mbit stream 🙂

2. Do the math

  • In local meetup you reach the 30 people in the room
  • With live Google Hangout you can reach 100 or so people if you advertise the event beforehand in your community medias
  • With YouTube recording you can reach all those 5000+ people who were not there in time or space when it happened

… this is not good only for the local community, but also good in general to have high quality recordings of your presentation to share later on for anyone.

3. The disadvantages of Google Hangout

  • You need a Google+ account for sharing your webcam or screen (your soul, real name, I know…)
  • The hangout organizer must be able to bind his/her Google+ account to YouTube account with real name policy permanently
  • … Google is little shortsighted here: you may need to create fake non-person G+ credentials for meetup organizations (now I hear Eric Schmidt crying)

Image

 

4. Using Hangout with Google Apps user account

If your organization is using Google Apps for email you may be able to enable both YouTube and G+ in your domain settings. After this, there still exist real name policy problems and you cannot use a name like “Secretary of Python Finland” in G+. So better come up with a foreign fake name… Also settings the name in G+ will silently enforce this name on Gmail and other Google services, so be careful.

Another warning: YouTube cannot be enabled for Google Apps accounts in all countries, so check this beforehand before trying to create an organizational G+ account. Finland was such a country. Rogue Gmail account, here I come…

Image

5. Preparing for live Google Hangout broadcasting

Start preparing a day before the event! As you can see the account policy and such may lie down obstacles on your way to become a screencast ninja. As sad it is, you need to practice the technical aspects of Hangout thing to make sure it works when the great day comes.

The encoding of live video will max out at least one of your CPU cores. Make sure you have powerful enough hardware under your fingers when you share your screen.

First your Google identities and cookies will become messed up in your web browser if you try to use G+ and/or Youtube with several Google accounts at once. In theory Google has some clever cookie tossing to tackle this problem, but in practice it just doesn’t work. Use Multifox Firefox extension and open a new identity window where the only logged in Google user is your Hangout user.

Image

Make sure all your presenters have their G+ account created beforehand and you have added them on your friend list. Make sure that the presenters have test drived the screensharing on their personal laptops – it would be shameful if streaming would fail because of something like a Linux driver bug. You will need to install a browser plug-in for the video encoding: I know at least Google Chrome and Firefox work, but I don’t suspect other major desktop browsers should have any problems (Lynx, Iceweasel and others, sorry again…)

To test things out, ask people to come to a test Hangout session where you really see their screensharing is working with you and you learn how to use Cameraman feature (more below).

6. Creating your Hangout

Just login to Google, click G+ and then Hangout > Create hangout.

A new pop-up window ensures. You need to also accept some additional site policy and Google warns you that you must have permission to broadcast and record the material. Make sure you obtain necessary permission from the presenters beforehand.

Image

Image

7. Starting the screencast

Hangout offers options to stream your webcam, screencast any of your monitors or screencast a particular window. There doesn’t seem to be option for a floating head on the top of presentation yet.

Image

 

8. On Air

(oblig. related Reckless Love music video)

The broadcasting can be public or private (only for invited hangout participants). The latter doesn’t scale well when you try to make it as pain free as possible for others to see the presentation online.

When you go public “on air” you’ll receive the YouTube short URL which has live broadcasting as Flash video widget (no HTML5 or WebRTC yet, sorry!). No YouTube login is needed in order to see the video in this URL: you can share it in IRC, Twitter and other social networks for your audience.

Image

Image

As the summoner of the Hangout, you have a Cameraman feature which controls who of the participants “is live” in the main live stream. You can switch the stream between any of the presenters and your local webcam, just for the audience to say hi for the presenters.

Image

Image You have a group chat feature  in Hangout. But often the chat is best to handle offband, like in IRC, where your target audience naturally come together online and you have better moderation tools in your possession.

Image

Please note that public broadcasting may attract unwanted attention. My fellow friends in Bitcoin Hackathlon got flooded over by kids when they were experimenting with Hangout. You can directly share your Hangout URL from the web browser’s address bar, but it means that anyone using that URL can join in Hangout for chat and video streaming.

9. Ending the Hangout and video postprocessing

After you press “terminate the call” icon in the top right corner, you’ll get a message telling that the recording of the live stream will be uploaded to YouTube.

Image

You can edit the recording later on in YouTube.

10. Bonus photo

I found a lovely Google Effects panel in Hangout. You can play sounds (drums, applause) or glue artifacts on the top of live video stream. It’s Movember and I seem to be victorious.

Image

 

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

Opening files from Firebug in Sublime Text 2 or any text editor

I have the following use case in my web application development workflow

Image

Effectively you right click any source code line in Firebug on any web server and you can open the corresponding source code line it in your favorite text editor. This also works for Javascript stack traces.

Image

Firebug has an external editor feature. However the external editor configuration does not support mapping URLs to files. Since I do stuff like WebGL and AJAX I cannot work with file:// protocol, but I use Python’s SimpleHTTPServer as a development server. I had to figure out a way to map URLs (http://localhost:8000) from Firebug to my local files on the hard drive (/Users/mikko/code/xxx) .

Thus, I created the Python script below. It takes four arguments URL (supplied by Firebug), line number (supplier by Firebug), base URL and base directory to map URLs to files on the hard disk. You can create several Firebug external editor configuration entries for different URLs.

On OSX, Firebug is picky and does not allow pass arbitrary UNIX binaries as external editor. Thus, you need to wrap the script using py2app or download the ready firebug-subl app (Mountain Lion) from Github and drop it in /Applications.

I also run for the two following bugs in Firebug 1.10.4 (not reported by me yet)

  • Cannot open the external editor if the URL starts with http://localhost (see the script below how to configure /etc/hosts for a workaround)
  • Cannot open the external editor in some cases if there are spaces in the command line (had to use | as the separator)

… caused some hair pulling.

Firebug’s external editor configuration menu can be hard to find:

Image

And here is a sample config:

Image

Further instructions in the script itself.

Note that the script outputs to /tmp/firebug-subl.log as there didn’t seem to be a way to get console output from the external editor otherwise. If this file doesn’t get created Firebug doesn’t even try to run the script (or the wrapper app) as with the bugs mentioned above.

And then the cake, firebug-subl.py. A possible updated version will appear on ztanesh repo on Github:

"""

    Firebug - Sublime Text 2 connector.

    Allows you to open any Firebug files in Sublime Text 2 text editor.
    Maps URLs to files.

    Usage::

        firebug-subl [url]|[line-no]|[base-url]|[base-directory]

    Note that we use pipe separation because of Firebug 1.10.4 seems
    to have a bug with space separation.

    Example configuration line in Firebug editor settings::

        %url|%line|http://firebugbugs:8000|~/code/mixnap-base/krusovice-src

    ... this will open all files from http://firebugbugs:8000 as they
    were on the directory /Users/mikko/code/mixnap-base/krusovice-src on the disk.

    Note: Firebug seems to have a bug that if the domain name in the URL
    is localhost it doesn't even try to start the editor. Thus you need
    to resolved /etc/hosts tricks to spoof your localhost with some other
    name if you run a local development server::

        127.0.0.1   localhost firebugbugs

    Shell expansion supported.

    Starting Firefox from command-line for debugging::

        /Applications/Firefox.app/Contents/MacOS/firefox

    Because of Firebug's Select application retardness this script must be wrapped with py2app on OSX
    before Firebug allows you to pick this script as a legal choice.
    In clean virtualenv::

        # We need py2app trunk version for OSX Mountain Lion as the writing of this (altgraph > 0.10)
        # First install hg command (mercurial)
        pip install setuptools-hg
        pip install -e hg+https://bitbucket.org/ronaldoussoren/altgraph#egg=altgraph
        pip install -e hg+https://bitbucket.org/ronaldoussoren/macholib#egg=macholib
        pip install -e hg+https://bitbucket.org/ronaldoussoren/modulegraph#egg=modulegraph
        pip install -e hg+https://bitbucket.org/ronaldoussoren/py2app#egg=py2app
        py2applet firebug-subl.py && cp -r firebug-subl.app /Applications

"""

__author__ = "Mikko Ohtamaa <http://opensourchacker.com>"
__license__ = "MIT"

import os
import sys
import subprocess
import logging
import urllib

# Write debug output to a file as we don't otherwise get any feedback
# if this script fails
logger = logging.getLogger("firebug-subl")
hdlr = logging.FileHandler('/tmp/firebug-subl.log')
logger.addHandler(hdlr)
logger.setLevel(logging.INFO)

# Add your installation here if missing
LOCATIONS = [
    "C:\\Program Files\\Sublime Text 2\\sublime_text.exe",
    "/home/ed/apps/sublime_text_2/sublime_tex",
    "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl"
]

def guess_subl():
    """
    Guess the editor location.
    """
    for loc in LOCATIONS:
        if os.path.exists(loc):
            return loc

    return None

def main():
    """
    Main magic.
    """

    logger.info("Got command-line: %s" % sys.argv)

    # Where subl lives?
    editor = guess_subl()
    if not editor:
        logger.error("Could not find Sublime Text 2")
        return

    # Read command-line
    # Here we need to have some hacks as for some reason space
    # space separated command line was broken on Firebug 1.10.4
    mega_arg = sys.argv[1]

    # Seems to be urlfied...
    mega_arg = urllib.unquote(mega_arg)

    url, line, base_url, base_dir = mega_arg.split("|")

    # Map Javascript file location from URL to a file on a disk
    path = url.replace(base_url, base_dir)

    # Replace tilde with the user home dir
    path = os.path.expanduser(path)

    if not os.path.exists(path):
        logger.error("Tried to open non-existing file %s - please check your URL-directory mapping" % path)
        return

    # Create a Sublime Text 2 style direct line in a file pointer
    hint = path + ":" + line

    logger.info("Launcing %s with %s" % (editor, hint))

    # Call Sublime Text to open the file in the current project.
    # Create process with shell variable expansion
    subprocess.call([editor, hint], shell=False)

try:
    main()
except Exception as e:
    logger.exception(e)

–

 

 

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+