Skip to content

Recent Articles

11
Dec

Running 1Password and Evernote under Seamonkey

I always find it fun to test new gadgets, and play with different ways of reaching the world.
I enjoy swapping phone platforms, OS, but mail clients and web-browsers are particularly susceptible to this, since they’re in that special fit of frequently used, but low cost-of switching.

Most recently, I’ve switched back to using a browser I used to love 10 years ago – Mozilla, or as it’s known now, Seamonkey [1]

Seamonkey logo

Seamonkey is the new name for what used to be called the Mozilla Application Suite, or, more commonly, Mozilla.
Like it’s predecessor Netscape 4, Seamonkey combines a Browser, Email client, and Newsreader into one svelt package. Even though the Mozilla Foundation quasi-abandoned Seamonkey back in 2005, volunteers around the world have kept it up to date.. Since it shares the engine with Firefox, it supports all major web features, and gives a really nice experience.

I had a lot of fun playing nostalgically with various Throbbers Ns ani, before getting down to the business of making it a mean, lean, modern web-browser.
Amusingly, with the resurgence of minimalism, the theme that was used during the beta looks perfectly at home.

Screen Shot 2013 12 10 at 6 01 44 AM

Modifying Extensions

As much fun as I was having, if I wanted to stay with Seamonkey for a primary browser, I couldn’t use it without extensions.
I’ve become far too accustomed to storing pages to read offline, using separate passwords for every site, and other things that aren’t built into any browser directly.

While quite a few extensions run out-of-the-box on Seamonkey, it’s often ignored compared to it’s vulpine cousin.
Luckily, they both use the same underlying engine, as well as the same UI framework, so porting them isn’t that bad.

Evernote

The first addon I ported over was the Evernote Web Clipper – It’s a cute little extension which lets me download webpages, and store them to read later.

Since it isn’t designed to run in Seamonkey, Mozilla (understandably) tries to discourage downloading the extension, but thankfully they do provide an override.

Download Extension

Once an extension is downloaded, it’s very straightforward to edit.
xpi extensions can be thought of as .zip files. [2]

By default, the files will unzip into your working directory, rather than a subdir (aka, a tarbomb)
This means that we should create a new subdirectory to work in

unzip evernote_web_clipper-*.xpi -d evernote-tmp/

The layout of modern extensions is pretty straightforward – The first file we care about is ‘install.rdf’
This file gives browsers the details of the extension – What browsers it’s compatible with, which languages it supports, etc.

Inside the file, there’s a section that describes it’s Firefox compatibility.

<!-- Firefox -->
<em:targetApplication>
  <Description>
    <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
    <em:minVersion>4.0b1>/em:minVersion>
    <em:maxVersion>24.0a1</em:maxVersion>
  </Description>
</em:targetApplication>

The section gives gives the Firefox ID, along with versions that are known to be compatible.
It’s straightforward to create a new entry just under it, and include that information for Seamonkey.

<!-- SeaMonkey -->
<em:targetApplication>
  <Description>
    <em:id>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</em:id>
    <em:minVersion>2.0</em:minVersion>
    <em:maxVersion&gt2.1.*</em:maxVersion>
  </Description>
</em:targetApplication>

That that that’s added, Seamonkey is willing to try to load the extension.
There’s one other change that needs to be made – It comes up fairly commonly when porting over extensions.

Firefox references the UI internally as ‘chrome://browser/content/browser.xul’, whereas Seamonkey uses ‘chrome://navigator/content/navigator.xul’

Thankfully, it’s easy to update this everywhere using some bash magic, without needing to manually chech each file.

    cd evernote-tmp
    for i in `find .`; do sed -i '' 's/chrome:\/\/browser\/content\/browser.xul/chrome:\/\/navigator\/content\/navigator.xul/g' $i; done > /dev/null 2>&1

Almost all extensions will need that fix. There are some other similar tweaks that other extensions might need, but it’s rare.
With that change in place, the extension should work fine in Seamonkey – It just needs to be turned back into an xpi file.

zip -r ~/Downloads/Evernote-for-Seamonkey.xpi *

Once this is an xpi file, it can be loaded like any other Extension that we downloaded from Mozilla.org.
Screen Shot 2013 12 10 at 6 24 59 AM
This adds the extension to Seamonkey, but by default doesn’t add it to the active toolbar.
It can be added by right-click the menu bar, choosing customize, and dragging the new button into the toolbar.

Screen Shot 2013 12 10 at 6 29 02 AM

Once that’s done, we’re good to go – The extension is in place, and we can properly save articles for reading later on.
Screen Shot 2013 12 10 at 6 30 05 AM

1Password

Another extension I rely heavily on is 1Password. It lets me generate unique passwords for each site I visit, so if they’re hacked, and my password is leaked, it can’t be used to break into my other accounts.
Converting it to work with Seamonkey is very similar to the procedure for Evernote –

Download the xpi file locally.

Download1Pass
Unzip into a tmp dir

unzip 1Password-4.0.1.xpi -d OnePass-tmp/

Edit install.rdf, and add the Seamonkey entry.

<!-- SeaMonkey -->
<em:targetApplication>
  <Description>
    <em:id>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</em:id>
    <em:minVersion>2.0</em:minVersion>
    <em:maxVersion&gt2.1.*</em:maxVersion>
  </Description>
</em:targetApplication>

Then change the browser.xul to navigator.xul, just like before.

    cd OnePass-tmp
    for i in `find .`; do sed -i '' 's/chrome:\/\/browser\/content\/browser.xul/chrome:\/\/navigator\/content\/navigator.xul/g' $i; done > /dev/null 2>&1

The 1Password extension has a few other places it’s looking for Firefox specifically that need to be tweaked.
It ships with a config file, ‘harness-options.json’, which sets various options.
One option, ‘is-sdk-bundled’ should be around line 4 – It chooses if the extension should use the version that’s built into firefox, or the one that ships with the plugin.

Setting this to true will ensure we load the version we’re about to modify.

 "is-sdk-bundled": true,

One of the files in the extension, ‘resources/addon-sdk/lib/sdk/loader/cuddlefish.js’ does another compatibility check.
Around line 50, there is a line that starts with ‘function incompatibility(module)’.
This can be bypassed in the terminal

sed -i '' 's/function incompatibility(module) {/function incompatibility(module) {return null;/g' resources/addon-sdk/lib/sdk/loader/cuddlefish.js

Once that’s fixed, it can be bundled up as an xpi, and installed via the gear icon in the admin menu, just like Evernote.

zip -r ~/Downloads/1Pass-for-Seamonkey.xpi *

While this loads, it’s not quite smooth sailing yet –

Screen Shot 2013 12 10 at 7 21 09 AM

In Version 4 of 1Password, Agilebits added a new security feature.
The app checks the gatekeeper signature of the browser that loads it, to ensure that it’s an approved browser.
The feature makes some sense – 1Password works by communicating with the browser over a socket, so adding some security to ensure we know who we’re talking to is reasonable.
Unfortunately, Seamonkey isn’t currently signed, so even if Agilebits wanted to, they couldn’t include it’s signature.

Of course, that’s no reason to give up ;)
I dug around in the app a bit – While I’m not really very talented at reverse-engineering apps, it looked to be fairly simple.
1Password runs in the background, listening on a socket, which the extension then connect to – When a connection comes in, 1Password looks up the Process that is talking to it.
This information is readily available in the system normally, such as with a lsof

2BUA8C4S2 34434 TCP localhost:10196->localhost:49738 (ESTABLISHED)
2BUA8C4S2 34434 TCP localhost:10191 (LISTEN)
2BUA8C4S2 34434 TCP localhost:10191 (LISTEN)
2BUA8C4S2 34434 TCP localhost:10196 (LISTEN)
2BUA8C4S2 34434 TCP localhost:10196 (LISTEN)

Once it has the PID, it can lookup the gatekeeper signature. Since all the major browsers are signed, it’s straightforward to compare against those signatures.

Screen Shot 2013 12 10 at 5 26 43 AM
Screen Shot 2013 12 10 at 5 32 09 AM

I can see where they’re going with this, and it’s an interesting idea, but the feature does make it a bit more difficult to use unsupported browsers.
Luckily, before I went too far down the rabbit hole of trying to patch the binary, I found an much easier way.
The new beta version has a bypass option built in.

Screen Shot 2013 12 10 at 7 35 49 AM

Success!
Seamonkey runs really well, and porting over a few plugins, even if you need to smash them around a bit, makes a great everyday experience.
I certainly wouldn’t recommend it for everyone, but it’s become my browser of choice — At least for this week.

Screen Shot 2013 12 10 at 5 41 06 AM

Footnotes

1 – I know that Mozilla prefers to refer to the suite as SeaMonkey, with both words capitalized. Personally, I find this a bit ugly, so I’ll be using the variant Seamonkey here.
2 – Technically, these are Jar files, but the distinction isn’t meaningful here.

3
Jul

Enable 1Password for Opera

I’m a big 1Password fan, and it’s one of the things which kept me from using Opera 12.15 and below.
The new Opera is based on Chromium however, so it’s sorta-kinda compatible with Chrome extensions ;)

1PassScreenshot

Note- I was able to install the extension using the instructions I’ve written up below.
I can’t guarantee it’ll work for you. It might very well corrupt your 1Password file, or do nasty things.
It will also be overwritten (and need to be run again) once Agilebits updates their Extension.
In Short – YMMV.

1Password extension for Opera 15

  1. First off, we’ll need to download the 1Password Chrome Plugin, from Agile Bit’s site
  2. Make sure you click the “Allow beta extensions” button.
    AllowBeta
    Then, click the download button next to “Google Chrome”.
  3. Opera will say *”Extension was disabled because it is from unknown source. Go to the Extension Manager to enable it.”*, click “OK”.
  4. ExtensionBlocked

  5. Go to Window, then select Extensions, to open the Extension Manager.
    ExtensionsMenu
  6. One there, the 1Password extension will be greyed out.
    Press Install.

    Screen Shot 2013 07 02 at 5 56 20 PM

  7. It will prompt you again in a little pop-up. Press install again.
    AllowInstall

    Make a note of the version- In the case of our screenshot, it’s such as 3.9.20.2
    Version
    If the plugin updates later, and this version has changed, you’ll need to re-run these steps.

  8. Close both the Opera Extensions window, and the Agilebits “Extension Setup” Tab.
    Go to File, and choose “Close Tab” for both tabs.
    CloseThis2
    If you do not do this step, when Opera re-opens it will re-open the Agilebits Extension installer , and re-install the default version of the extension.

    This would undo the changes we make below, so make sure you close this ;)
  9. Quit Opera. Make sure you go to the Opera menu, and choose Quit, even if no webpages are showing.

    Quit

  10. Open the Terminal (It’s in Applications, Utilities)
    This will allow us to perform the additional steps necessary to load the extension.
    You can also open the terminal through Spotlight if that’s easier ;)
  11. Move to the directory where your Opera Extensions are installed.
    Enter the following command into the Terminal, and press enter.

    cd "/Users/`whoami`/Library/Application Support/com.operasoftware.Opera/Extensions"
    
  12. Next we need to locate the 1Password extension we just installed.
    The exact name changes, so we want to look for it in the “Info.plist” file that is distributed with the Extension.
    The following “Find” command will search for it, then move to it’s directory.

    cd $(dirname $(ls -th `grep -r "AgileBits" --include "Info.plist" * -l` | head -n 1))
    
  13. Chrome has an “Omnibox” which combines the URL bar with Search.
    Opera has something similar, but doesn’t let extensions talk to it yet – So we need to remove this code from the Extension, so it can load in Opera.
    Run the two commands below in the Terminal.

    cat code/global.min.js | python -c "import sys,re;a = re.sub(r'chrome\.omnibox(.*?)a.message\)}','',sys.stdin.read(),flags=re.MULTILINE|re.DOTALL); print(a)" > tmp
    cp tmp code/global.min.js
    
  14. Re-open Opera.
    You should be all set at this point – The extension will need to sync, which might take a minute or two after clicking on the icon.
    You might also find you have luck opening or closing the window which loads the extension.

    Notes-

    I had initially thought that it would be a lot more complicated to port the extension, but Opera does not seem to have changed the internals as much as I thought.
    For instance, 1Password uses tests such as

    if("object"==typeof chrome) { alert("foo")}
    

    These work with “chrome”, not “opera”.
    The most complicated part is that Chrome 15 does not have the Omnibar property.

    I’m not an expert, but there appears to be a few other cases where the extension could be updated to use more recent methods, such as moving sendRequest -> sendMessage, or chrome.tabs.getSelected -> chrome.tabs.query.
    In all, it appears to work, however.

12
Feb

Transitioning from a DIV to Table-based layout. On purpose.

I’ve been working on a site that uses a App-model style layout;

3 columns, like an Email client.

Screen

It works using Divs laid out with CSS, and generally works really well.

It even loads well on older version of Netscape, properly degrading to run, even if it’s not as pretty.
NS4

The more I look at it, though, the more I wonder if there is more I could do.
If dropping support for ancient browsers is really necessary at all.

Given that it’s a simple 3 column site, my initial thought was to experiment with frames. While these work, dataURI+frames aren’t as well supported as I’d like, and the experience of frames on lynx is not ideal.

What about Tables?

Tables have gotten a bad rap in recent years.
It is generally recommended that we avoid tables, and use divs for layout. But why?

There are few traditional arguments that are given against tables:

  • HTML should be semantic
  • It’s often argued that HTML should be semantic where possible, and to split the presentation (CSS) from the layout (HTML). In practice, however, this often doesn’t work out. To make various tricks work right, developers are often creating a wide variety of presentational divs anyway, for wrappers and other non-semantic content.

    <div class="region region-content">
    <div id="block-system-main" class="block block-system">
    <div class="content">
    <!– Actual page content here –>
    </div>
    </div>
    </div>

    You could certainly argue that we’re moving in a more semantic direction, and HTML5 certainly moves in that direction, but we’re still a long way away from there.
    Combine that with other work arounds that are being done to support non-performant browsers

    [if IE 6]>

    and it’s hard to justify that a simple table is much worse.

  • Screen Readers can’t handle Tables
  • Modern screen readers are actually fairly decently at handling tables. They’ve improved in the last 10 years since we started using CSS.

    Additionally, you can mark a table as presentational, so that it can be interpreted as such using role=”presentation”.

    JAWS, WindowEyes, and NVDA will all do the right thing.

  • CSS lets you just change the one CSS file, rather than every page
  • If I were manually editing 100s of files, this might be a valid concern – But like many modern developers, I’m writing web applications designed to run with a templating system – Python, Ruby, even PHP have libraries which make this trivial. I can change my structure in a single location, and have it automatically apply on every pageload.

  • Table Layouts are only used by people who can’t do divs properly
  • I can sympathize with this view- There’s a lot of people who get stuck in CSS land, and resort to a “Screw it, I’ll use a table” attitude, so I can see where it comes from. But Google and Twitter are using tables as part of their layouts, and I’m sure they have someone there who knows what they’re doing ;)
    In my case, I have the layout working properly in CSS, but I’m deliberately redoing it, spending extra time, to gain the broad-compatibility that a table-based layout brings.
    Some things are easier, some things are harder, but it’s a valid choice in designing a site.

But CSS is really useful!

It sure is!
And I’m in favor of using CSS, all over the place!
Gradients are better than images, rotation is amazing, and changing elements through properties is useful.
CSS has it’s warts (Templating would be nice..), but it’s a great way to explain how elements should look. And divs are often useful for layout, too.
But non-tabular use of a tables is probably OK, as part of a well-balanced breakfast.

29
Jan

Using Mechanical Turk to build a library of Grocery Cards

Envelopes

Earlier this year, I wanted to add the ability to generate and display barcodes as part the Savingstar iPhone app, similar to Loyalty card wallets.
We’re already storing loyalty card numbers of over a million users in order to process their coupons – It seemed like regenerating these into scannable barcodes should be simple enough.
For stores with a scanner that can read a phone (an increasing number), this is a lot more convenient.

Looking over the various card ranges, they fell into a few formats- EAN-13, UPC, etc.

Luckily, there is a library which can easily write these, and easy to use python bindings in order to use them.

Barcode

After a few trials however, I ran into a small issue-

The numbers in the barcode don’t always match up with the printed card number!

Often there is a prefix/postfix on the number, in addition to the checkdigits for UPCs.
After thinking about it for a while, I realized I could reverse engineer the formats, but I’d need cards. Lots of cards.

I turned to Mechanical Turk.

Getting Cards

I posted several jobs, each time asking people to mail me loyalty cards.
Because I needed to match the cards to a something digital to approve/deny them on Mechanical Turk, generated hundreds of random strings, and assigned one to each user.
I then asked each user to then write their particular random string on the envelope; When I received the envelope, I could match it up and give them credit for the job.

And we got cards! Hundreds of beautiful cards, dozens of cards in each of the ranges I needed.
People from around the US and Canada send cards from every major chain in the US.

The biggest downsize to this method is that there is a built in lag.. So even after I had enough cards for each certain chains, and pulled the listing, I still received loads of cards that had already been mailed ;)

In App

12
Dec

Mount KVM images on Linux

Just a quick little script I use once in a while, which I need to mount my KVM images, to access the files manually.

The way it works is to calculate where the data starts, then simply pass that as a paramter to the mount command.

simple, but very useful! ;)


#!/bin/bash
if [ $# -lt 2 ]
        then
        echo "Usage $0 image mountpoint"
        exit
fi

IMAGEFILE=$1
MOUNTPOINT=$2

SECTORSIZE=$(parted -s $IMAGEFILE unit s print | grep "Sector size" | awk -F": " {'print $2'} | awk -F"B/" {'print $1'})
OFFSETBLOCKS=$(parted -s $IMAGEFILE unit s print | grep "^ 3" | awk {'print $2'} | awk -Fs {'print $1'})
OFFSETSECTORS=$(echo "$SECTORSIZE * $OFFSETBLOCKS"  | bc)
mount -o loop,offset=$OFFSETSECTORS $IMAGEFILE $MOUNTPOINT

10
Dec

How to write a simple ajax game loop in Javascript

A friend of mine from the Java world has been trying to get into JS lately.

He understood the basics of how JS works – Functions look (on the outside) superficially similar to Java, it has standard loops/variables, etc.
Canvas functions are very straight forward- The part that confused him was “How do I create a game loop”, and “How do I update the page based on JSON responses from my server?”

Luckily, both of those are REALLY easy in Javascript!

I’m certainly not a Javascript expert, but I put together a simple demonstration, with an over-the-top number of comments.
GameShot

We’ll start with our very simple HTML page.
Anyone who’s done even “Hello World” should understand most of what we’re doing here.

We’re loading our JS, then three divs – Header, Game, and Footer.

<html>
<head>
    <title>This is an example</title>

    <!-- 
         Include are our JS scripts. The "defer" line just tells it to run this after the page loads.
         It's not actually needed, but it makes the initial load faster 
         We're loading in jQuery via a CDN, but copying it locally would work just as well.
         This is just one less file to deal with.
     -->

    <script defer src="http://code.jquery.com/jquery.js"></script>
    <script defer src="exampleCode.js"></script>
</head>

    <!-- 
        In the body (below), define the game however you want it to look.
        I'm using 3 divs, just for the sake of showing we're only showing/changing the game div.
        There's nothing special about this setup.
     -->

<body>
    <div id="Intro">
        This is the beginning of the game.
    </div>
    <div id="Game">
        The GAME GOES HERE.
    </div>
    <div id="Footer">
        This is the bottom.
    </div>
</body>

Now this is calling our JS, exampleCode.js –
That’s also very simple. We’re just going to do two “game loops” to update the page.
One of which updates the color, and the other uses an AJAX request


// This function does the logic of updating our game div.
// Obviously, in a real game, this would be more interesting.
function ChangeColor()
{ 

  // in jQuery, the $ character gives you the root jQuery object.
  // You can use the $(FOO) notation gives you a reference to any element
  // Any element you want. It's just like a CSS selector.
  // So-
  // $('#foo') gives you a HTML element with id="foo"
  // $('.foo') gives you every element with the class="foo" (loop through them)

  // The next line creates an array of random colors.
  bgcolorlist= Array("#DFDFFF", "#FFFFBF", "#80FF80", "#EAEAFF", "#C9FFA8", "#F7F7F7", "#FFFFFF", "#DDDD00");

  // Now, Randomly choose one, by picking a random number (between 1 and arraysize), and then get that array element.
  randombgcolor = bgcolorlist[Math.floor(Math.random()*bgcolorlist.length)];

  // Now, in jQuery, you can set a CSS property, by using the .css function.
  // I'm using it to set the background for the HTML element #Game.

  $("#Game").css('background',randombgcolor);

}

// Let's try a slightly more interesting function. We'll add an external call.
function UpdateTime()
{ 

  // Now, let's do something Ajaxy-
  // There is a URL endpoint for Tavern that gives us the server status, including the time.
  // We can use that to display the current time on our page.

  // The $.ajax() function doesn't return the JSON.. It RUNS the success value as a function.
  // That function will save the variable for us.

  // Normally, we could use $.get(), or $.getJSON(), but these just call $.ajax() under the hood.
  // By calling $.ajax() directly, we can set more options.

  // set the result variable ahead of time, so it's scope isn't only inside the function.
  var result;


  // make the AJAX call
  $.ajax({
    // async defaults to True- This means that the result will wait for a return before running anything else.
    async: false,
    // Setting the dataType to JSON is a convenience. It automatically parses it into a JSON object for us.
    dataType:"json",
    // This is the URL we're pulling from. I set this up to be accessible from any domain.
    // Normally, you can only make requests to the SAME DOMAIN as the HTML runs from.
    // But you can bypass this with a header (Access-Control-Allow-Origin), which I've set for you.
    url: "http://gettavern.com:8090/status",

    // This is just our super-simple function that gets called on success.
    // ALL it's doing is setting the result back to that variable we declared before.

    // Normally, in JS, you'd just do whatever it is you actually needed with your drawing/etc, right in the inline function..
    // Or have it call a longer function, that's defined normally (like the one we're in!)
    // But in this case, I wanted to a) illustrate Inline functions.
    //                           and b) make it short ;)
    success: function(data) {
        result = data;
    }
  });




  // Now, make that into something printable. Notice how we pull values out of the JS call as if it were a dictionary/hashmap?
  timeStampString = "The current timestamp is " + result['timestamp'] + ".";

  // Now, let's DO something with this data-
  // create a new javascript Date object based on the timestamp
  // multiplied by 1000 so that the argument is in milliseconds, not seconds
  var date = new Date(result['timestamp']*1000);
  // hours part from the timestamp
  var hours = date.getHours();
  // minutes part from the timestamp
  var minutes = date.getMinutes();
  // seconds part from the timestamp
  var seconds = date.getSeconds();

  // will display time in 10:30:23 format
  var formattedTime = "
This is equal to " + hours + ":" + minutes + ":" + seconds; // This redefines the HTML inside the game object to whatever we pass it. In this case, it's the time. // Get the Game's DOM object // This gets the instance of the Game, just like we were in the first function. game = $("#Game"); // Replace the Game's HTML. // Just like used the background function in the first example (above), here we're overwriting the HTML that's in the Game object. // Everytime this runs, everthing that is in the game div is erased, and replaced with whatever string we pass. // In this case, we're passing in our two time-based strings we just created. game.html(timeStampString + formattedTime); } // Run the code above, every 500ms self.setInterval("ChangeColor()",500); self.setInterval("UpdateTime()",1000);

You can see this in action at jsfiddle.

It’s all very very straightforward code, but it occured to me that it might be useful to others, so I’ve archived it here for anyone who finds it ;)

5
Dec

iStat Menus demotes user’s licenses with sneaky upgrade

For the last few years, I’ve been using a cute little tool called iStat menus to graph my CPU/Memory in my address bar.
It doesn’t do that much, but it works, it stays out of my way, and the UI is better than the nearest free alternative.

Menu2

I don’t think about the tool very much – Once in a while I’ll glance up when the system is running slow, or use the clock function to see the time in multiple time-zones.

Every so often, there’s a new version available, and the software prompts me to upgrade –

Upg1 2

Upg2 3
I generally go ahead and agree to the change, and go about my business.

Although my 1999 self would cry to hear me say it, I don’t always read through the changelogs-

We get them everywhere now – On my Android Phone (Yep, go ahead, update), iPhone (Sure. Update), OSX (OK, add the new release), etc.

In my professional life managing servers, I review the changes, weigh the pros and cons, and decide if I want to upgrade – But for a simple Menu app, that seems overkill.

This week, iStat released another upgrade, and once more I upgraded without really paying much attention.
I probably should have, but realistically, I don’t think most users will.
Upg3

As you can see in the screenshot however, this upgrade to iStat Pro 4 is a paid upgrade.
Accepting this upgrade downgraded my paid software to 14-day trial status.

Trial

I’m not the only one who felt that wasn’t cool.

iStat is offering upgrade pricing ($9), and the app isn’t that expensive in and of itself, but the entire process felt very sketchy.[1]

Users are used to accepting in-app upgrades, and switching one out for a paid-upgrade feels very disingenuous, regardless of their justifications.
Bjango (the creators) could have handled things differently,by setting the “Remind me Later” Dialog option to be the blue/rightmost option, or even installing as a different name in the Applications folder, so you could still easily fire up v3.

Admittedly, It’s a minor issue, $9 isn’t much, and I don’t begrudge an indie shop trying to raise money for a new version and stand in business, but the sense of fairplay has been violated- It’s one more reason for me to try to avoid buying non App Store applications in the future.

For anyone else who runs into this, You can uninstall iStat 4 using the menu item-
Uninstall
And then reinstall Version 3.


[1]For what it’s worth, I did end up buying the upgrade. The UI is a bit nicer.
This isn’t about the $9. It’s about the process.

28
Nov

Fun with Framesets in 2012

One of the things I’ve noticed in web development is a trend toward avoiding older technologies.

There’s often a good reason for this – Newer replacements allow more flexibility, work more reliably, and are better supported.

But that doesn’t mean we should always abandon old friends – In this case, I mean the humble HTML frame.



Although deprecated in HTML5, there’s still fun to be had.

But aren’t frames evil?

One of the biggest criticisms of frames is that they aren’t reliably accessed – Their very nature allow search-engines (or users) to link to one specific frame, rather than to the frameset container you’re wrapping them with.


It turns out, we have a weapon against this, which has been around since 1998- The DataURI.

Rather than linking to a specific file, using the datauri, we can embed our frame elements directly INSIDE the original page.
They can be generated at the same time, with the rest of your code, and stored directly.
This eliminates the idea of a separate page that might ever accidentally get linked to.

<frameset cols="30%,*">
        <frame src="data:text/html;base64,CjxodG1sPgogICAgPGhlYWQ+CiAgICAgICAgPHRpdGxlPkZyYW1lIE1lbnU8L3RpdGxlPgogICAgICAgIDxiYXNlIHRhcmdldD0iY29udGVudCI+CiAgICA8L2hlYWQ+CiAgICA8Ym9keSBiZ2NvbG9yPSIjMDA5OUREIj4KICAgICAgICA8YSBocmVmPSJodHRwOi8vd3d3LnlhaG9vLmNvbSIgY2xhc3M9Im1lbnUiPllhaG9vLmNvbTwvYT48YnIgLz4KICAgICAgICA8YSBocmVmPSJodHRwOi8vd3d3Lmdvb2dsZS5jb20iIGNsYXNzPSJtZW51Ij5Hb29nbGUuY29tPC9hPjxiciAvPgogICAgICAgIDxociAvPgogICAgICAgIDxhIGhyZWY9Imh0dHA6Ly93d3cuZWJheS5jb20iIGNsYXNzPSJtZW51Ij5FYmF5LmNvbTwvYT48YnIgLz4KICAgICAgICA8YSBocmVmPSJodHRwOi8vd3d3LmZhbmRhbmdvLmNvbSIgY2xhc3M9Im1lbnUiPkZhbmRhbmdvLmNvbTwvYT48YnIgLz4KICAgIDwvYm9keT4KPC9odG1sPgo=">
        <frame src="data:text/html;base64,CjxodG1sPgogICAgPGhlYWQ+CiAgICAgICAgPHRpdGxlPkZyYW1lIENvbnRlbnQ8L3RpdGxlPgogICAgPC9oZWFkPgogICAgPGJvZHkgYmdjb2xvcj0iI0RERERERCI+CiAgICAgICAgPHA+SGVyZSdzIHdoYXQgaXQgd291bGQgbG9vayBsaWtlQSBnb29kIHJ1bGUgb2YgdGh1bWIgaXMgdG8gY2FsbCB0aGUgcGFnZSB3aGljaCBjb250YWlucyB0aGlzIGZyYW1lIGluZm9ybWF0aW9uICJpbmRleC5odG1sIiBiZWNhdXNlIHRoYXQgaXMgdHlwaWNhbGx5IGEgc2l0ZSdzIG1haW4gcGFnZS4KICAgICAgICA8cD5BIGdvb2QgcnVsZSBvZiB0aHVtYiBpcyB0byBjYWxsIHRoZSBwYWdlIHdoaWNoIGNvbnRhaW5zIHRoaXMgZnJhbWUgaW5mb3JtYXRpb24gImluZGV4Lmh0bWwiIGJlY2F1c2UgdGhhdCBpcyB0eXBpY2FsbHkgYSBzaXRlJ3MgbWFpbiBwYWdlLjwvcD4KICAgICAgICA8cD5BIGdvb2QgcnVsZSBvZiB0aHVtYiBpcyB0byBjYWxsIHRoZSBwYWdlIHdoaWNoIGNvbnRhaW5zIHRoaXMgZnJhbWUgaW5mb3JtYXRpb24gImluZGV4Lmh0bWwiIGJlY2F1c2UgdGhhdCBpcyB0eXBpY2FsbHkgYSBzaXRlJ3MgbWFpbiBwYWdlLjwvcD4KICAgICAgICA8cD5BIGdvb2QgcnVsZSBvZiB0aHVtYiBpcyB0byBjYWxsIHRoZSBwYWdlIHdoaWNoIGNvbnRhaW5zIHRoaXMgZnJhbWUgaW5mb3JtYXRpb24gImluZGV4Lmh0bWwiIGJlY2F1c2UgdGhhdCBpcyB0eXBpY2FsbHkgYSBzaXRlJ3MgbWFpbiBwYWdlLjwvcD4KICAgICAgICA8cD5BIGdvb2QgcnVsZSBvZiB0aHVtYiBpcyB0byBjYWxsIHRoZSBwYWdlIHdoaWNoIGNvbnRhaW5zIHRoaXMgZnJhbWUgaW5mb3JtYXRpb24gImluZGV4Lmh0bWwiIGJlY2F1c2UgdGhhdCBpcyB0eXBpY2FsbHkgYSBzaXRlJ3MgbWFpbiBwYWdlLjwvcD4KICAgICAgICA8cD5BIGdvb2QgcnVsZSBvZiB0aHVtYiBpcyB0byBjYWxsIHRoZSBwYWdlIHdoaWNoIGNvbnRhaW5zIHRoaXMgZnJhbWUgaW5mb3JtYXRpb24gImluZGV4Lmh0bWwiIGJlY2F1c2UgdGhhdCBpcyB0eXBpY2FsbHkgYSBzaXRlJ3MgbWFpbiBwYWdlLjwvcD4KICAgICAgICA8cD5IZXJlJ3Mgd2hhdCBpdCB3b3VsZCBsb29rIGxpa2VBIGdvb2QgcnVsZSBvZiB0aHVtYiBpcyB0byBjYWxsIHRoZSBwYWdlIHdoaWNoIGNvbnRhaW5zIHRoaXMgZnJhbWUgaW5mb3JtYXRpb24gImluZGV4Lmh0bWwiIGJlY2F1c2UgdGhhdCBpcyB0eXBpY2FsbHkgYSBzaXRlJ3MgbWFpbiBwYWdlLgogICAgICAgIDxwPkEgZ29vZCBydWxlIG9mIHRodW1iIGlzIHRvIGNhbGwgdGhlIHBhZ2Ugd2hpY2ggY29udGFpbnMgdGhpcyBmcmFtZSBpbmZvcm1hdGlvbiAiaW5kZXguaHRtbCIgYmVjYXVzZSB0aGF0IGlzIHR5cGljYWxseSBhIHNpdGUncyBtYWluIHBhZ2UuPC9wPgogICAgICAgIDxwPkEgZ29vZCBydWxlIG9mIHRodW1iIGlzIHRvIGNhbGwgdGhlIHBhZ2Ugd2hpY2ggY29udGFpbnMgdGhpcyBmcmFtZSBpbmZvcm1hdGlvbiAiaW5kZXguaHRtbCIgYmVjYXVzZSB0aGF0IGlzIHR5cGljYWxseSBhIHNpdGUncyBtYWluIHBhZ2UuPC9wPgogICAgICAgIDxwPkEgZ29vZCBydWxlIG9mIHRodW1iIGlzIHRvIGNhbGwgdGhlIHBhZ2Ugd2hpY2ggY29udGFpbnMgdGhpcyBmcmFtZSBpbmZvcm1hdGlvbiAiaW5kZXguaHRtbCIgYmVjYXVzZSB0aGF0IGlzIHR5cGljYWxseSBhIHNpdGUncyBtYWluIHBhZ2UuPC9wPgogICAgICAgIDxwPkEgZ29vZCBydWxlIG9mIHRodW1iIGlzIHRvIGNhbGwgdGhlIHBhZ2Ugd2hpY2ggY29udGFpbnMgdGhpcyBmcmFtZSBpbmZvcm1hdGlvbiAiaW5kZXguaHRtbCIgYmVjYXVzZSB0aGF0IGlzIHR5cGljYWxseSBhIHNpdGUncyBtYWluIHBhZ2UuPC9wPgogICAgICAgIDxwPkEgZ29vZCBydWxlIG9mIHRodW1iIGlzIHRvIGNhbGwgdGhlIHBhZ2Ugd2hpY2ggY29udGFpbnMgdGhpcyBmcmFtZSBpbmZvcm1hdGlvbiAiaW5kZXguaHRtbCIgYmVjYXVzZSB0aGF0IGlzIHR5cGljYWxseSBhIHNpdGUncyBtYWluIHBhZ2UuPC9wPgogICAgICAgIDxwPkhlcmUncyB3aGF0IGl0IHdvdWxkIGxvb2sgbGlrZUEgZ29vZCBydWxlIG9mIHRodW1iIGlzIHRvIGNhbGwgdGhlIHBhZ2Ugd2hpY2ggY29udGFpbnMgdGhpcyBmcmFtZSBpbmZvcm1hdGlvbiAiaW5kZXguaHRtbCIgYmVjYXVzZSB0aGF0IGlzIHR5cGljYWxseSBhIHNpdGUncyBtYWluIHBhZ2UuCiAgICAgICAgPHA+QSBnb29kIHJ1bGUgb2YgdGh1bWIgaXMgdG8gY2FsbCB0aGUgcGFnZSB3aGljaCBjb250YWlucyB0aGlzIGZyYW1lIGluZm9ybWF0aW9uICJpbmRleC5odG1sIiBiZWNhdXNlIHRoYXQgaXMgdHlwaWNhbGx5IGEgc2l0ZSdzIG1haW4gcGFnZS48L3A+CiAgICAgICAgPHA+QSBnb29kIHJ1bGUgb2YgdGh1bWIgaXMgdG8gY2FsbCB0aGUgcGFnZSB3aGljaCBjb250YWlucyB0aGlzIGZyYW1lIGluZm9ybWF0aW9uICJpbmRleC5odG1sIiBiZWNhdXNlIHRoYXQgaXMgdHlwaWNhbGx5IGEgc2l0ZSdzIG1haW4gcGFnZS48L3A+CiAgICAgICAgPHA+QSBnb29kIHJ1bGUgb2YgdGh1bWIgaXMgdG8gY2FsbCB0aGUgcGFnZSB3aGljaCBjb250YWlucyB0aGlzIGZyYW1lIGluZm9ybWF0aW9uICJpbmRleC5odG1sIiBiZWNhdXNlIHRoYXQgaXMgdHlwaWNhbGx5IGEgc2l0ZSdzIG1haW4gcGFnZS48L3A+CiAgICAgICAgPHA+QSBnb29kIHJ1bGUgb2YgdGh1bWIgaXMgdG8gY2FsbCB0aGUgcGFnZSB3aGljaCBjb250YWlucyB0aGlzIGZyYW1lIGluZm9ybWF0aW9uICJpbmRleC5odG1sIiBiZWNhdXNlIHRoYXQgaXMgdHlwaWNhbGx5IGEgc2l0ZSdzIG1haW4gcGFnZS48L3A+CiAgICAgICAgPHA+QSBnb29kIHJ1bGUgb2YgdGh1bWIgaXMgdG8gY2FsbCB0aGUgcGFnZSB3aGljaCBjb250YWlucyB0aGlzIGZyYW1lIGluZm9ybWF0aW9uICJpbmRleC5odG1sIiBiZWNhdXNlIHRoYXQgaXMgdHlwaWNhbGx5IGEgc2l0ZSdzIG1haW4gcGFnZS48L3A+CiAgICA8L2JvZHk+CjwvaHRtbD4KCg==">
    </frameset>

See this in action

If your browser supports the “data:text/html;charset=UTF-8,” type (webkit seems to like this better than others) you can make this even easier, and embed your normal HTML, straight into the original page.

This will work somewhat similarly to a resizable-div, but using 1998-level technology.


<frameset cols="30%,*">
        <frame src="data:text/html;charset=UTF-8,<html>
    <head>
        <title>Frame Content</title>
    </head>
    <body bgcolor='#DDDDDD'>
        <p>Here's what it would look likeA good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>Here's what it would look likeA good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>Here's what it would look likeA good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
        <p>A good rule of thumb is to call the page which contains this frame information "index.html" because that is typically a site's main page.</p>
    </body>
</html>">
        <frame src="data:text/html;charset=UTF-8,
<html>
    <head>
        <title>Frame Menu</title>
        <base target='content'>
    </head>
    <body bgcolor='#0099DD'>
        <a href='http://www.yahoo.com' class='menu'>Yahoo.com</a><br />
        <a href='http://www.google.com' class='menu'>Google.com</a><br />
        <hr />
        <a href='http://www.ebay.com' class='menu'>Ebay.com</a><br />
        <a href='http://www.fandango.com' class='menu'>Fandango.com</a><br />
    </body>
</html>">
    </frameset>

See this in action

Why would I use this?

Honestly, I don’t think that anyone should ;)
But it does have certain advantages over “Splitter” style javascript -

  • It’ll work without JS enabled
  • It’s compatible with old browsers
  • The browser does the work of resizing, rather than requiring it to happen in JS



In all, it’s a fun hack, and I figured it might appeal to other people who haven’t forgotten about some of the older layers still with us in our browsers.

Of course, while it might work in Netscape 3.0, like anything fun, it doesn’t work in older versions of IE.

24
Oct

LinkedIn’s new Endorsement system is Viral SPAM. And that’s why it works.

Late last month, in an effort to increase user-engagement, LinkedIn launched a new feature which lets you ‘endorse’ your connections with one click.

This is intended to be a short-hand way of recommending people, designed to get the lazy among us to attest to the skills of their former coworkers. It might get clicks, but they’re meaningless noise.


It starts

The process starts when one of your colleges ‘endorses’ you using the new system.
This sends an email to your inbox, which looks vaguely interesting.

Endorsed

You click to investigate, and you’re presented with a list of your friends, asking if they have certain skills.
Endorse

The UI makes it easy to mass-endorse your friends without even reading the dialogs, or to endorse each person with a single click.

After endorsing a skill, the dialog replaces them with with another skill for another one of your connections.

Endorsing them, however, just perpetuates the cycle. They will then receive the same email you did.
They’ll click and log in, thinking that someone might have had something meaningful to say about them, but be presented with the annoying Viral mechanism that caught you earlier.

Nowhere to run

While I appreciate that LinkedIn is trying to drive people to the site, encouraging contact-spamming seems one of the cheesiest ways to do so.
There is currently no way to disable endorsements entirely, and Unendorsing is a tedious process, requiring you to go to each profile manually.


I understand that LinkedIn is trying to ensure that LinkedIn is a tool people use every day, but this makes me more likely to simply killfile them in the future.

24
Oct

Pulling the Mixergy archive locally

MixergyLink

Mixergy.com is a great resource for interviews with various entrepreneurs, both famous and up-and-coming.

I generally enjoy listening to the interviews, but I recently I’ve begun trying to exercise more, and I’ve been enjoying listening to his podcast while I walk the trails.

Unfortunately, the podcasts are only available for a limited period of time, before they become premium only.

I don’t mind paying in order to download the back archives, but it’s not a straight fee-for-product transaction. At least when I last looked at it, you could only download individual interview files if you found them, one page at a time.

That’s great if you’re trying to download a single interview and listen to it, but if you want them all on your iPod to choose while walking or driving, it’s less than ideal.

Being an command line guy, I realized this should be a simple problem to solve using some bash scripting. I’m sure I could have done it in Python just as easily, but since I’m in the terminal anyway, Bash is a great Go-To solution to problems ;)

Logging in

Mixergy uses cookies for authentication, storing a login token, and then checking for it when you try to download a file.

This makes a lot of sense, and is straightforward to work with.


I logged in using Firefox, and exported out my cookies for mixergy.com, then saved them out to a file using a Firefox plugin.
I could then use this for the next set of requests.

Acquire List of Interviews

Since I couldn’t find a list of all the interviews on one page, I had to crawl backward on the news blog, harvesting each link.

I noticed that Mixergy always linked each interview in it’s own page, with “Read More” as the anchor text.

I tested pulling these links in, and it seemed to work reliably.

# Generate list of interviews

curl -L –cookies cookies http://mixergy.com/interviews/page/1/ | grep “Read more” | awk -F\” {‘print $4′}

This seemed to give me the list of interview-specific pages pretty well, so I iterated this out to the rest of the pages in the archive, moving through each page of the search results.

I added a sleep in between requests to avoid hammering the server.

# Pull ALL the Interview URLS.

for i in `seq 1 62`; do curl -L –cookies cookies http://mixergy.com/interviews/page/$i/ | grep “Read more” | awk -F\” {‘print $4′} >> interviewpages; sleep 1; done

I then used very similiar logic to extract out the specific pages for the classes.

#Do the same for the classes.

for i in `seq 1 6`; do curl -L –cookies cookies http://mixergy.com/premium/page/$i/ | grep “<h2><span>”| awk -F”a href=” {‘print $2′} | awk -F\” {‘print $2′} >> classes; sleep 1; done

Looking through these, I now had a list of URLs, each of which contained the text of the interview, and a link to the MP3 version.

Acquire Each Audio File



At this point, I just had to extract the links to the mp3s.
I tested with a single page-

#Generate list of MP3s

curl -L –cookies cookies http://mixergy.com/eddy-lu-grubwithus-interview/ | grep mp3 | awk -F “a href” {‘print $2′} | awk -F\” {‘print $2′}

This seemed to work – It gave me a URL to a single MP3.

I then rolled this through each of the single-interview pages I had downloaded before, to find the URLs of all MP3s.

#Get the MP3 list

for i in `cat interviewpages`; do sleep 1; curl -L –cookie cookies $i | grep mp3 | awk -F “a href” {‘print $2′} | awk -F\” {‘print $2′} >> interviewmp3 ;done

This gave me a list “interviewmp3″ which contained a direct link to each file.
From here, it was a simple matter to loop through and download each one.

# Retrieve all MP3s.
for i in `cat interviewmp3`; do sleep 1; wget $i; done

And Success! I downloaded hundreds of startup interviews, and can load them to whatever devices I choose, and listen to them whenever I want.

Mixergy

Follow

Get every new post delivered to your Inbox.