Welcome!

Welcome to Kirsle.net! This is the personal website of Noah Petherbridge, and it's where my various software projects and web blog lives.

Principle of Least Astonishment

Noah Petherbridge
kirsle
Posted by Noah Petherbridge on Tuesday, March 22 2016 @ 12:29:07 PM

In user interface and software design, the principle of least astonishment states that "if a necessary feature has a high astonishment factor, it may be necessary to redesign the feature." It means that your user interface should behave in a way that the user expects, based on their prior knowledge of how similar interfaces behave.

This is a rant about Mac OS X.

At my new job, they gave me a Macbook Pro to work on because they standardized around Apple products. At first I figured I would just get used to OS X, and it was difficult not to because this Macbook didn't take very well to Linux (most notably, its retina display causes problems in Linux; Linux can deal with high-density displays, but it's currently quite difficult to connect an external standard DPI monitor and get everything to scale correctly).

I ran OS X full-time at work for about three months and came to the conclusion that Mac developers apparently don't care at all for the principle of least astonishment. (Note that this isn't a jab at Apple, because most of these problems come from popular third party apps, but perhaps Apple gave apps more control than they should have over window management features).

(I eventually had them swap out my Macbook Pro with a standard HP laptop on which I installed Linux).

The OS X Dock and Window Switching

One of my biggest sources of frustration is the dock and window switching features of OS X.

The dock groups all windows of a like application into one single icon on the dock. If you have multiple windows of the same app open, clicking its dock icon will bring all of the windows to the front. Which window then has the keyboard's focus is up to the alignment of the stars. There's no option to ungroup windows into different icons.

To switch windows you hit Command-Tab (⌘Tab), which is like Alt-Tab in Windows and Linux except it only switches between applications and not windows. When you ⌘Tab to an app, again all windows of that app come to the foreground and you can't predict which of its windows has your keyboard's focus.

  • Example: Adium. Adium regularly has at least two windows open: the buddy list window and the chat window. Every single time I ⌘Tab to Adium, the buddy list window takes the focus, even if the chat window was the last one I used. I always have to then ⌘` (to switch windows between the current application) to focus the chat window.
  • Example: iTerm2. If I had two iTerm windows open, then every single time I ⌘Tab to iTerm, it would focus the opposite window from the one I had last time. For example I could ⌘Tab from iTerm "A" to Firefox to iTerm "B" to Firefox to iTerm "A". This was particularly frustrating because usually when I have two iTerm windows open it's because I want one to sit mostly idle; maybe it's watching system resources or running a long task and I didn't want it to take up a tab on my main window.
  • Example: Minecraft. Every time I would ⌘Tab to and from a Minecraft window, the Command key would become "stuck" within Minecraft. For example I'd ⌘Tab to it and start typing stuff in chat, but when I type the letter "a" it behaves as though I typed ⌘A to select all text and then I'd subsequently end up deleting it. To remedy this I'd have to again ⌘Tab away and back again from Minecraft to "unstick" the key.

Another example is Google Chrome, but this one can't be summed up with just one bullet point.

Chrome has Web Apps, and for example we'll pick on the Hangouts app. Web apps get their own icon inside of the Chrome Web Apps launcher, and when you open them, they get their own icon on your OS X dock. This sounds good in theory, but under the hood the web app is still running as Google Chrome and OS X's window management problems still apply.

Even though you have two icons on your dock (Chrome and Hangouts), they both refer to the same application. If you ⌘Tab to Chrome, Hangouts also comes to the foreground. If you ⌘Tab to Hangouts, Chrome also comes up. You still have all the same problems of not knowing which window will grab the focus when you ⌘Tab to either app. For example, if you had Hangouts active earlier, and later you ⌘Tab to Chrome, the Hangouts window gets the keyboard focus even though you wanted the Chrome window.

It appears that it is up to the individual app how they deal with their app being switched to. Some apps focus the last window you were working from, some apps focus the next window in their own list, some apps always focus a specific window every time.

You can't simply learn one time how window switching and the dock works and apply that knowledge to every app you run. If you run a new app for the first time, you can't predict how it will behave without testing it. This is a violation of the principle of least astonishment.

The Zoom Button

The zoom button is on the window title bar of most apps, and it's roughly analogous to the "maximize" button on Windows and Linux.

As of Mac OS X Yosemite and El Capitan, the default behavior of the Zoom button is to put the current app into full screen mode (not maximized). If you just wanted to maximize the window instead, you have to hold the Option key while clicking on the Zoom button, or else double-click the title bar of the window.

Most apps behave this way, but some do their own crazy thing instead.

  • Example: Google Chrome. Trying to maximize Chrome using the standard way that works in most apps does not in fact maximize Chrome. It only expands it vertically, but not horizontally. To maximize Chrome, you have to push Option+Shift+Zoom (or Option+Shift+double click the title bar).
  • Example: a past version of iTunes. I'll admit that this example is outdated, but the way iTunes behaved on Mac OS X Leopard was equally surprising. Back then, the Zoom button maximized most apps by default, except in iTunes, when it would instead shrink iTunes down into a minimalistic playback control window. The window would actually get significantly smaller instead of larger when you click the Zoom button.

I don't have other examples because thankfully my set of applications I work with mostly did The Right Thing™.

The point of my complaint about the Zoom button is that it's not possible to simply learn one time about what the Zoom button does and how to maximize a window. It appears that every app is capable of overriding the behavior of this button and do their own thing with it. If you run a new app for the first time, you can't know in advance what will happen when you click this button. This is a violation of the principle of least astonishment.

The Command Key and Other Behaviors

The command key in general is a source of many problems.

There are a few Command-key shortcuts that globally apply to all apps. These are shortcuts like ⌘X, ⌘C, and ⌘V for cut, copy and paste. Besides this, it is apparently possible for apps to bind their own actions to the command key, and one app's set of keyboard shortcuts won't necessarily be the same in another, similar, app.

  • Example: Adium again. In most apps, if you want to insert a literal line break character in a text box (where hitting Return/Enter would send the message), you would hold down Shift when hitting Return. For example, Slack behaves this way. But in Adium, this will cause the message to be sent anyway. Instead you have to hold Option and hit Return; or was it Command? I'd always screw this up three different ways before figuring it out again.
  • Example: Atom vs. all other text interfaces I used. In Atom, hitting the Home key would put the cursor at the beginning of the current line of text, and End would put it at the end of the line. In most other apps with large text areas, Home instead puts the insertion cursor at the beginning of the entire document, and End at the end of the document.
  • Example: iTerm2 and Atom's tab switching shortcuts. In iTerm2 the way to switch from one tab to the next was to hit ⌘+Left or Right arrow. In Atom, this shortcut doesn't do anything. Instead you'd do Control-PageUp or PageDn, which is more similar to how apps like Atom behave on other platforms besides OS X. I haven't tested tab switching shortcuts in other apps.

The one good thing I liked about the Command key is that it made it easy to copy and paste text from a terminal. Control-C has a special function in terminals, so being able to copy without halting the running application was a neat feature. In Linux land most terminal emulators make you push Shift-Ctrl-C to copy text.

The general feeling I got from the use of the Command key on Mac OS X is that nobody can agree on what it should be used for. Every developer uses it differently in their app, and sometimes there's no rhyme or reason for why one shortcut binds to Command instead of to Control or Option. I feel that if Linux developers decided they wanted to add a new modifier key for shits and giggles, the same sort of absolute mess would follow. Instead, for the most part, nobody in Linux makes very much use of the Command key at all (in Linux the Command key is called Super, and is the same as the Windows key on a standard keyboard). In most Linux desktop environments nothing at all is bound to the Super key, although the GNOME Shell desktop makes some use of it for window switching purposes.

In contrast, the Windows key on a Windows OS is very well defined. There are a handful of global Winkey shortcuts (for example, Windows+R to open the Run dialog, or Windows+L to lock your screen). These shortcuts apply globally throughout the OS, and no third party software that I'm aware of binds their own shortcuts to the Windows key. If that's even possible for an app to do, I don't know. Mac OS X would be better off not allowing application writers to bind custom actions to the Command key because they clearly can't be trusted with it.

This keyboard shortcut mess is a violation of the principle of least astonishment.

I'll probably update this blog post with additional examples as I find them. I still dual-boot my personal Macbook Air with Linux and OS X.

Managing Your $GOPATH for Multiple Go Projects

Noah Petherbridge
kirsle
Posted by Noah Petherbridge on Friday, March 18 2016 @ 11:40:34 AM

This is a cool tip I picked up from checking out other peoples' Go projects.

When you're new to Go, the documentation tells you about $GOPATH which tells Go where to install packages and where the source codes to your project and its dependencies live. A lot of people might set $GOPATH to be $HOME/go, and work on their projects out of ~/go/src/github.com/myname/myproject.

Using one global GOPATH like this has its problems though:

  • You have to git clone your projects into ugly directory paths to make sure they're inside the GOPATH. I usually prefer to just have a folder like ~/git/myproject instead, and I don't want to symlink it into my GOPATH so that Go finds it.
  • The third-party dependencies of all of your Go projects get intermingled in the same GOPATH. This is especially problematic if two different projects need the same third-party dependency, but they use different versions of that dependency.

So, the solution I found to this problem is to use a Makefile for your project that creates a private GOPATH for you. Here's an example Makefile from one of my projects:

CURDIR = $(shell pwd)
GOPATH = "$(CURDIR)/.gopath"
all: build

# `make setup` to set up git submodules
setup:
    git submodule init
    git submodule update

# `make run` to build and run the bot
run: gopath
    GOPATH=$(GOPATH) GO15VENDOREXPERIMENT=1 go run cmd/scarecrow/main.go

# `make fmt` runs gofmt
fmt:
    gofmt -w src

# `make build` to build the binary
build:
    GOPATH=$(GOPATH) GO15VENDOREXPERIMENT=1 \
        go build -i -o scarecrow cmd/scarecrow/main.go

# Sets up the gopath / build environment
gopath:
    export GO15VENDOREXPERIMENT=1
    mkdir -p .gopath/src/github.com/aichaos/scarecrow bin
    ln -sf "$(CURDIR)/src" .gopath/src/github.com/aichaos/scarecrow
    ln -sf "$(CURDIR)/vendor" .gopath/src/github.com/aichaos/scarecrow/src

My project's directory structure is laid out like this:

/
  cmd/
    scarecrow/
      main.go
  src/
    scaregrow.go
    utils.go
    configs.go
    logging.go
  vendor/
    github.com/
      aichaos/
        rivescript-go/...
      mattn/
        go-xmpp/...

With this project layout, the bulk of my source code is under the /src directory (to keep the root of the repo clutter-free), the actual executable program is under the /cmd directory, and third-party dependencies are under /vendor (using the Go 1.5 vendor experiment).

Instead of running go run cmd/scarecrow/main.go, I type make run. My Makefile then generates a custom private GOPATH at /.gopath, makes symbolic links to all the relevant files underneath, and runs the program from the context of that GOPATH.

The ~/.gopath directory structure looks like this:

/.gopath
  /src
    /github.com
      /aichaos
        /scarecrow
          /src -> ../../../../../src (link)
            /vendor -> ../../../../../../vendor (link)

This way the private GOPATH has all the necessary directory structures that Go needs to find my project's source code and its vendored modules, and I don't have to clutter my global GOPATH. Also, this makes it easy to just git clone my project any place I want, like ~/git/scarecrow and not worry too much about the Go configuration.

As a side note, with the Go 1.5 vendor experiment I used git submodules to add the third-party dependencies to my project. On a fresh git clone I just type make setup which initializes and clones the submodules.

Categories:

[ 0 comments | Add comment | Permalink ]

Sudo-less NPM Global Installs

Noah Petherbridge
kirsle
Posted by Noah Petherbridge on Friday, March 18 2016 @ 10:53:06 AM

If you're a JavaScript developer (in the Node.js world), you're probably used to typing in commands like sudo npm install -g gulp-cli (unless you installed Node via Homebrew on Mac OS X so that your local user account owns /usr/local). But I never liked the idea of having files installed under /usr/local (root-owned or otherwise) that my package manager didn't know about, and would prefer to have these global npm commands installed into a user-specific root instead.

Here's how to set this up.

First, edit your .bashrc (or .bash_profile or .zshrc) to add some environment variables:

# Node/npm
export NPM_PACKAGES="${HOME}/.npm-global-pkg"
export NODE_PATH="${NPM_PACKAGES}/lib/node_modules:${NODE_PATH}"
export PATH="${NPM_PACKAGES}/bin:$PATH"

What this does:

  • $NPM_PACKAGES is a variable we use so we don't have to define the path more than once (Node/npm doesn't use this variable itself). We want our global modules to be installed under ~/.npm-global-pkg
  • $NODE_PATH is used by Node when you require() a module; if the module isn't in the local project's directory it will try to import it from our new global directory.
  • We update our $PATH to add ~/.npm-global-pkg/bin, so when we install a global npm package that contains a command line program (like gulp, coffee, bower, etc.) it's available as a command in our shell.

Then, create or edit the file ~/.npmrc to specify the same path from $NPM_PACKAGES as your npm prefix:

prefix = ${HOME}/.npm-global-pkg

This tells Node where to install packages when you run npm install -g.

Restart your terminal session or re-source your configuration so the environment variables take effect. Now, when you run a command like (the sudo-less!) npm install -g coffee-script, it will place its binary in ~/.npm-global-pkg/bin/coffee.

Android 6.0 "Battery Optimization"

Noah Petherbridge
kirsle
Posted by Noah Petherbridge on Wednesday, January 13 2016 @ 12:18:39 PM

PSA: The "battery optimization" feature in Android 6.0 Marshmallow may be causing your Gmail to not notify you of e-mails until it's way too late and other apps to not notify you about anything ever.

I first caught on to this when I stumbled upon a Reddit thread on /r/Android. Essentially, Android 6.0 added a feature called Doze which puts apps to sleep while the phone is locked/not in use, giving them only brief windows of opportunity to check for any new notifications before being put back to sleep. The frequency between these wake opportunities depends on how long the phone has been locked. It sounds like a good idea in theory, but the execution was done very poorly.

That thread was specifically about apps like Gmail and Inbox. Users were seeing issues where Gmail wouldn't notify them about new e-mails until several hours after the e-mails came in. This is because the Gmail app was getting "dozed" and was unable to check for any new messages (or receive push notifications, or whatever it does). The work-around is to disable the "battery optimization" for the Gmail app.

To do so:

  1. Go to your Settings and then Battery
  2. In the menu on the right, go to Battery Optimization
  3. It lists the non-optimized apps and those that can't be optimized. In the drop-down at the top, pick All Apps
  4. Pick Gmail and any other app you care about and pick "Don't optimize"

Doze makes it sound like not optimizing apps will hurt your battery life. This may be true for poorly written apps, but it's pretty safe to allow Gmail to not be put to sleep. Before Android 6.0, Doze wasn't even a feature anyway and there weren't a lot of complaints about battery life. Personally, I just disabled battery optimization for a small handful of apps that I care more about, including Gmail and Hangouts.

In addition to Gmail, a few other apps I found to be completely broken when they're being "optimized":

  1. Google Opinion Rewards: Get Google Play credit for answering surveys. I noticed I wasn't getting any surveys offered for several weeks, and figured the Doze feature was to blame. I disabled it on the Google Opinion Rewards last night, and got a survey this morning.

    My theory is that this app only receives surveys if there is one available at the time the app asks for it. Since the app was checking very infrequently, it was missing all the surveys.

  2. Tasker: I have a Tasker task set up to automatically connect to my VPN when my phone joins certain WiFi networks. I was noticing that it was failing to do this the majority of the time. I'd have to open Tasker and manually play the Connect VPN task and then it would sorta work, but sometimes it would fail to run the Disconnect VPN task when I left the WiFi network.

    Disabling battery optimization for Tasker fixed this problem.

  3. Tumblr: I was getting no notifications at all from this app, ever. Disabling battery optimization fixed this problem.

  4. Reddit Sync Pro: I was getting no notifications of new messages on this app until I manually opened it. Again, disabling the optimization helped.

So if you have any apps that have been oddly quiet for the past several weeks or months and you're running Android 6.0, check if the "battery optimization" feature is to blame.

Categories:

[ 0 comments | Add comment | Permalink ]

Let's Encrypt

Noah Petherbridge
kirsle
Posted by Noah Petherbridge on Wednesday, December 30 2015 @ 07:36:02 PM

The free SSL certificate authority Let's Encrypt went into public beta earlier this month, and I updated all of my sites to use SSL now. I still had several more months before kirsle.net's old certificate from Namecheap expired, but I switched to the Let's Encrypt certificate because I could include all my subdomains instead of only the www one.

Check out their website and get free SSL certificates for your sites, too. I'm just writing this blog with some personal tips for how I configured my nginx server and a Python script I wrote to automate the process of renewing my certificates every month (Let's Encrypt certs expire every 90 days).

nginx config

Because all of my sites now use SSL encryption, and nginx has a handful of options for configuring the cipher suites and other settings (you need to play around with these to get a good score on the SSL Labs security test), I created a file named /etc/nginx/ssl_params to put all of the common parameters. Then, each individual site includes that file.

/etc/nginx/ssl_params

# Common SSL security settings
ssl_session_timeout 5m;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_dhparam /etc/ssl/kirsle.net-2015/dhparams.pem;

# So the Acme client can use the htdocs method
location /.well-known {
    alias /var/www/html/.well-known;
}

Notice how I also made a global alias to point the /.well-known URI to the /var/www/html/.well-known path. This is useful for running the ACME client (I use the standard letsencrypt-auto client); I can use the "web root" method and then I don't need to stop my nginx web server (I don't trust a program to automatically edit my nginx config, and I don't want to bring down all my sites in order to run letsencrypt-auto in stand-alone mode, where it runs its own server temporarily).

And then, for my websites that use SSL certs, I include the ssl_params file so that I don't need to manage redundant settings in a dozen different places. Here's an example for my rpm.kirsle.net subdomain, which is where I put random Linux packages:

server {
    server_name rpm.kirsle.net;
    listen [2604:a880:1:20::46:5004]:443 ipv6only=on;
    listen 443 ssl;

    ssl on;
    ssl_certificate /etc/letsencrypt/live/www.kirsle.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.kirsle.net/privkey.pem;
    include ssl_params;

    root /home/kirsle/subdomains/rpm;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
        autoindex on;
    }
}

# Forward port 80 to SSL
server {
    server_name rpm.kirsle.net;
    listen [2604:a880:1:20::46:5004]:80 ipv6only=on;
    listen 80;
    return 301 https://rpm.kirsle.net$request_uri;
}

Get a certificate without stopping your web server

As touched on before, I didn't want to have to do service nginx stop in order to run the letsencrypt-auto client in standalone mode. (The reason I would have to is that many of my sites are Python web apps, so they don't have a traditional document root directory like some simpler sites would, such as many sites that would be hosted by an Apache server).

So for example, on Kirsle.net the majority of requests are handled directly by the Python app, so there is no root document directory (I make aliases for certain subdirectories, though, for serving static files because this is better handled by nginx than by Python). So, a workaround is to configure your nginx server to alias the /.well-known URI to a document root on disk, because the ACME client will want to create files underneath the /.well-known/acme-challenge/ directory to prove ownership of your domain name.

To keep things simple and DRY, I put that URI alias in the shared SSL parameters file.

Automatically renew certificates

Let's Encrypt is still in beta at the time of writing, and they haven't yet implemented a way to automatically renew certificates before they expire; and Let's Encrypt certificates expire every 90 days, unlike most traditional certificate authorities that give you 12 month leases at a time.

So in the mean time, I wrote a simple Python script for myself that renews all my certificates regularly. I installed it in my root user's crontab on my server, to run the script on the first of every month. The script checks all the existing certificates to see if any should be renewed (if they're at least a month old), and runs the letsencrypt-auto client to update them and restart my web server.

Here's the source code. For the raw plain text version, check https://sh.kirsle.net/renew-certs.py. There's a configurable section at the top of the script that can be edited to put in your own domain names.

#!/usr/bin/env python3

# Cron script to renew LetsEncrypt certificates.
#
# --Kirsle
# https://sh.kirsle.net/

import os
import subprocess
import time

################################################################################
# Configuration Section Begins                                                 #
################################################################################

# Let's Encrypt directories
LE_APPDIR = "/opt/letsencrypt"       # Where `letsencrypt-auto` lives
LE_CERTS  = "/etc/letsencrypt/live"  # Where live certificates go

# Common arguments to letsencrypt-auto
COMMON = ["./letsencrypt-auto", "certonly", "--renew",
          "--webroot", "-w", "/var/www/html"]

# Domains and their subdomains; one array element per certificate, with each
# array element being a list of domains to include in the same cert.
CERTS = [
    [ "www.kirsle.net", "kirsle.net", "www.kirsle.com", "kirsle.com",
      "www.kirsle.org", "kirsle.org", "sh.kirsle.net", "rpm.kirsle.net",
      "minecraft.kirsle.net", "mc.kirsle.net", "rophako.kirsle.net" ],
    [ "noah.is", "www.noah.is", "petherbridge.org", "www.petherbridge.org",
      "noah.petherbridge.org", "noahpetherbridge.com",
      "www.noahpetherbridge.com" ],
    [ "rivescript.com", "www.rivescript.com", "static.rivescript.com" ],
]

# Minimum lifetime for certificate before renewing it?
LIFETIME = 60*60*24*30  # Once a month.

# Command to run after finishing if certs were renewed.
RESTART_COMMAND = ["service", "nginx", "reload"]

################################################################################
# End Configuration Section                                                    #
################################################################################

def main():
    os.chdir(LE_APPDIR)

    # If any certs were renewed, we'll schedule the restart command at the end.
    any_renewed = False

    # See which certificates are ready to be renewed.
    print("Checking SSL certificates for renewal")
    for cert in CERTS:
        ready = False  # Ready to renew this one
        primary = cert[0]

        # Find its existing live certificate file.
        home = os.path.join(LE_CERTS, primary)
        chain = os.path.join(home, "cert.pem")

        # When was it last modified?
        if not os.path.isfile(chain):
            print("NOTE: No existing cert file found for {} ({})".format(
                primary,
                chain,
            ))
            ready = True
        else:
            mtime = os.stat(chain).st_mtime
            if time.time() - mtime > LIFETIME:
                print("Cert for {} is old; scheduling it for renewal!"\
                    .format(primary))
                ready = True

        # Proceed?
        if ready:
            print("Renewing certificate for {}...".format(primary))
            command = []
            command.extend(COMMON)

            # Add all the domains.
            for domain in cert:
                command.extend(["-d", domain])

            print("Exec: {}".format(" ".join(command)))
            subprocess.call(command)
            any_renewed = True

    # If any certs were changed, restart the web server.
    if any_renewed:
        print("Restarting the web server: {}".format(" ".join(RESTART_COMMAND)))
        subprocess.call(RESTART_COMMAND)

if __name__ == "__main__":
    main()

Kirsle
Channels
Creativity
Software
Web Tools
Subdomains
Miscellany
Links


Fan Club