Imposing Bans and App Store Sanctions

Brent Simmons wrote about imposing sanctions by making apps unavailable in certain countries (in his case: Saudi Arabia) in November 2018.

I never thought about the mere possibility of doing so. It’s an intriguing thought: even when politics don’t result in whatever you want, you can always be picky about who you do custom with. It’s a power we have, a power every producer and craftsperson has. Turn down a business for moral reasons.

Naturally, I talked this through with a friend. We both met through our philosophy studies at university, so I knew the discussion would bear fruit. He pointed out that imposing bans on a country level may have unintended consequences. Take China for example, a more recent bogeyman of sorts. Do you want to take your app from Chinese stores to “punish the system”? If so, you’ll also punish the investigative journalists in China, he pointed out. Your actions will have casualties.

Well, what a bummer. The power fantasy is intriguing: you churn out code and you can influence international politics and the world, in a way! – But unless you talk with every customer before their purchase, you’ll end up punishing all the wrong people, too. Collateral damage. I don’t like that a bit.

I don’t know why Brent asked about Saudi Arabia. Because their funding of terrorism? Who funds this? All Saudis? Does your app promote terrorism-funding? Do you really want to punish Saudi Arabia’s government instead of its people? What do you talk about when you talk about a country, really?

So for now I will not be imposing bans on any specific country, even though I could: FastSpring, my e-commerce provider of choice, provides the capabilities to impose filters. Not going to use them any time soon.

Free Amazon S3 and CloudFront Hosting for Students

If you’re a student and apply for GitHub Education, you get a lot of cool stuff for free.

Part of this cool stuff apparently is “aws educate” access, I was told:

GitHub Student Developer Pack members receive up to $110 in bonus AWS credits for a total of $75-$150

This means you can host your indie app downloads on S3 and CloudFront for free for a very long time. I wrote a tutorial on how to set up CloudFront to host downloads and potentially static websites. It details the transition of an existing S3 bucket to the modern access rights management and a cached CloudFront endpoint.

If you’re new to S3/CloudFront, you can start with these simple steps:

  1. Create S3 bucket: https://console.aws.amazon.com/s3/
    • Disable public access (all of it)
    • Create /public directory in the bucket
    • Upload a /public/index.html with a link back to your website
  2. Set up CloudFront cache: https://console.aws.amazon.com/cloudfront/

    You’ll be creating a CloudFront user (“Origin Access Identity”, with access to S3) and a distribution (public URL endpoint) Best create a new CloudFront Distribution and let the setup do all the work:

    • Create Distribution”, then select the “Web” target
    • Origin Domain Name: Select the S3 bucket target you created in step 1
    • Origin Path: /public
    • Restrict Bucket Access: (1) Yes, (2) Create a New Identity
    • Grant Read Permission: Yes
    • (Leave rest in between as is; ensure you use SSL certificate)
    • Default Root Object: index.html (path is relative to the origin path)

That’s it. Wait a while until everything is set up, test the URL, and you should see the contents of your index.html.

From bash to zsh on macOS

In anticipation of macOS 10.15 Catalina, I have changed my shell from bash to zsh. macOS 10.15 will use zsh as the new default, and I was pretty sure that things will break immediately unless I prepare – so I did prepare, and I found the transition very simple.

zsh in iTerm2 with the snazzy theme and pure installed

My old bash prompt didn’t work out of the box, so getting a decent prompt with some color and git repository information, I managed to set up sindresorhus/pure to offer asynchronous (!) info like the pwd’s git metadata.

If you want my setup, install this:

I had to split my old .bash_profile file into an .aliases file that is used by both bash and zsh; similarly, I extracted variable settings (like adjustments to my PATH) into a shared file. Both zsh and bash can “import” other shell files via the source FILENAME program. To split the configuration up and then migrating the old bash stuff this way proved to be very simple and satisfying.

I didn’t want to replace the .bash_profile because I want my dotfiles to be compatible with my webserver and other devices where zsh is not installed.

Browser-Like Navigation in The Archive

This weekend, I released an update to the note-taking app I’m working on for a while called The Archive. This update is pretty big for people not getting updates from the opt-in “Cutting Edge” update channel, because all of a sudden the app allows you to navigate back in time.

The navigation stack (what you’d call browser history available from the navigation buttons in your browser) behaves like this:

  • Select a note from the search result list, and it gets added to the navigation stack.
  • Type into the Omnibar to search. A snaptshot for the history will be created once you change the selection.

Restoration of a historic point in time means the same note you visited is visible again, and the search results are restored completely. Including stuff you might have deleted in the meantime. You could use this to restore notes, if you wanted.

Check out The Archive’s free 60 day trial. Experience this yourself.

The Archive now sports the well-established pattern of showing a back/forward arrow we all know and love from every graphical web browser, ever

It took a while to experiment with the rules for this. What counts as a “historic event” that should be recorded?

If you type a long search term into the app’s Omnibar, it wouldn’t make much sense to add each keystroke to the history. So that’s not a good candidate for a rule.

If the app remembered changes to search results instead of your keystrokes, the history would be less noisy and travelling back in time would be more meaningful, granted. It’s still too weird, though. Let me illustrate what I mean here: if you type to search for Zettelkasten, it’s very likely that the set of search results will stay the same after you typed Zettelk. So the subsequent searches for Zettelk, Zettelka, Zettelkas, Zettelkast, Zettelkaste, and Zettelkasten would not be stored in the history because the search results are the same for all of these (partial) search terms. But is the set of search results the most meaningful thing of the app? Is it what you want to go back to?

Eventually, we settled on getting back to changes of note selection.

If you select a note from the search results, it’s added to the history and you can go back to it. That feels pretty natural already, much like clicking on a link in a web browser results in a different page being shown, so you hit the back button to get to the previous page. We don’t tend to think about this and take it for granted; it’s a clear case.

But what about searches? The Archive sports a feature where it automatically shows the best match of your search result in the editor. Whenevery you follow a link from note A to note B, this is essentially what’s happening under the hood: note B’s title is put into the search, and B is the best match for its title, so it is displayed immediately. This interaction, following links, had to result in putting the note on the history stack as well.

Through this, we initially got adding the best match for a search when the user types for free. And it’s not weird at all. Often the best fit, displayed by the app, end up being what the user interacts with. And by “interact” I don’t mean editing the contents; looking at it is enough oftentimes. You want to get back to the note you just read a minute ago. So we kept this.

Navigating your notes in The Archive is feeling more and more natural with every update. I really like to work inside the app as much as I like working on the app. I take this as a good sign, and I sure hope the app brings delight to the work of others, too!

I cannot wait to see the next big features being implemented and talk to users about them. I really can’t. It’s a pity I’m the bottleneck here, darnit :)

Check out The Archive if you haven’t already and tell me what you think of the latest additions!

embetty Embeds Tweets and YouTube Videos Without the Tracking Code

Found embetty, and now I am reconsidering to embed marked-up Tweets and YouTube videos again. Embetty displays Tweets as proper cards, but without Twitters visitor-tracking code.

Instead of screenshots, they have a demo page.

You need to setup an embetty-server first, though. That’s kind of annoying, but not much of a problem for pros. I wonder if a shared server would do, or if that puts you into weird legal situations again, because the embetty-server proxy now becomes the de facto hub of displayed content but isn’t allowed to according to Twitter and YouTube rules, or something like that.

Python Context Manager Compared to Ruby DSLs

Python has the with keyword that is analogous to using in C#. It is used to manage resource access and wrap a block of code with setup and cleanup stuff. I like it.

Here’s an example adapted from the Python Enhancement Proposal (PEP) 343 that introduced the implementation in Python 2.6:

from contextlib import contextmanager

@contextmanager
def opening(filename):
   f = open(filename)
   try:
       yield f
   finally:
       f.close()

with opening("the/filename") as f:
    for line in f:
        print line

I like it so much because it reminds me of the block-based resource access I’m now used to thanks to Ruby and Swift spoiling me for years. I’m not a fan of repeating imperative sequences between opening/closing file handle code since I know you can do it this way.

I’m still learning Python. That’s why I sometimes try to come up with equivalent Ruby code (which is my reference language of choice to explain new concepts in familiar terms). I noticed that the Python example above sports the yield keywords, which is how blocks are called in Ruby, too. So I wondered what the @contextmanager decorator is useful for, and why the with keyword relies on the result of the expression to respond to the context manager protocol. This protocol is defined through the __enter__ and __exit__ methods being available. That’s how the block is wrapped.

In Ruby, there’s no language feature like decorators. These are basically function call wrappers, but you define the wrapping when you define the function, instead of at the call site. The code above is similar to contextmanager(open(filename)); that’s the gist of decorators as far as I know.

How do you implement the stuff above in Ruby then?

class MyFile
  def self.opening(filename)
    f = File.open(filename)
    yield f
  ensure
    f.close
  end
end

MyFile.opening("the/filename") do |f|
  f.each_line do |line|
    puts line
  end
end

That’s it, as far as I can tell. The (implicit) block passed to MyFile#opening is invoked with the yield keyword. There’s no built-in functionality apart from this. It’s similar to the Python code – but only on the surface level. Python has interesting things going on behind the scenes in the implementation of the with keyword, and then there’s the code for the @contextmanager decorator itself.

I think the crucial language difference here is that Ruby is designed to make writing block-based methods so easy. By design, Ruby encourages coming up with domain-specific languages (DSLs).

Python’s with keyword reads nicer, but then again you can write this as part of your DSL in Ruby, too, if you support optional blocks in the wrapped method. It does not make sense, but there you go:

class MyFile
  def self.opening(filename)

  end

  def self.opening(filename, &block)
    # Wrap the method body in a lamda so we can either run or return it
    impl = ->(&inner_block) { 
      f = File.open(filename)
      try
        # Explicit version of `yield`
        inner_block.call(f)
      ensure
        f.close
      end
    }

    # Return the lambda for later execution if this is 
    # not a direct call with a block
    return impl if block.nil?

    # If we pass a block, execute right away!
    impl.call(&block)
  end
end

def with(wrapped, &block)
  wrapped.call(&block)
end


MyFile.opening("the/filename") do |f|
  puts "regular:"
  puts f.readlines
end

with MyFile.opening("wrapped/fn") do |f|
  puts "wrapped:"
  puts f.readlines
end

How to Add Files to mpd using python-mpd2

The music player daemon, mpd, and its client counterpart mpc operate on a managed directory structure. All paths are relative to this root directory. You cannot make mpd play a file from just anywhere, it seems.

This is important to know when you script MPDClient using python-mpd2, because when you try to add any absolute path, even those pointing into the managed directory, you’ll be in trouble.

mpd.base.CommandError: [4@0] {add} Access denied

That’s all you’ll get for an absolute file://-style URL.

And if you just use the absolute path without a protocol specification, you get:

mpd.base.CommandError: [50@0] {add} Malformed URI

The trick is to remember to exclusively use relative paths.

Instead of e.g.

/music/mpd/files/The Artist/The Album/The Track.mp3`

you pass in

The Artist/The Album/The Track.mp3

and all will be fine.

To make sure you get this right, have a look at the output of mpc listall where all accepted relative file paths are listed.

You can use os.path.relpath to remove the path to the managed music library from an absolute path. See the following classes for an example.

Types to manage music libraries

import os

class Album:
    def __init__(self, rel_path):
        self.rel_path = rel_path

    def name(self):
        return os.path.basename(self.rel_path)

class Library:
    def __init__(self, lib_path):
        self.path = os.path.abspath(lib_path)
        self._guard_exists()

    def _guard_exists(self):
        if not (os.path.exists(self.path) \
                and os.path.isdir(self.path)):
            raise Exception("Music library directory does not exist at: %s" % self.path)

    def all_albums(self):
        """Returns a list of Album objects in the library subdirectories, sorted by name."""
        self._guard_exists()
        return [Album(rel_path=path)
                for path in sorted(os.listdir(self.path))
                if os.path.isdir(os.path.join(self.path, path))]

I then use Album objects to make the audio player switch audio books.

Emacs for Remote SSH Python Development

I am using Emacs for over a year now to manage my tasks. I like how I can mix tasks with long form notes in a single outline. It’s good.

Emacs project view with Solarized theme and neotree open to the right

We had to play with vi and emacs for a while at University. I’m very happy I got used to the very basics of both editors because I ended up using vi a lot when SSH-ing into remote machines, and now Emacs for everything else.

Accessing files on a remote machine with Emacs is painless. You visit a file (C-x C-f) and enter /ssh:user@host:path/to/file and off you go. Of course Emacs helps you to autocomplete every part of this when you press tab.

Navigating the same directory structure all the time got on my nerves; the emacs-neotree package helped on that front. Love it.

The Python project you see is my One Button Audio Player Raspberry Pi project. I’m writing the control software and will solder everything together next week. It’s an audio player with a single button to help my 90-year-old grandmother access audio books and music. She has about 2% vision left, so any consumer device you can buy is way too finicky. This should help. Hashtag giving back some love to your ancestors.

Mac App Store Proceeds for TableFlip Without Marketing Campaigns

Since I put TableFlip on the Mac App Store, I sell about 1 or 2 copies each day. I didn’t invest any time in marketing, yet, apart from the announcement post here plus a tweet. That’s why I think these sales are driven by search terms through the Mac App Store. Then again, maybe not, I cannot say.

For years I hesitated to put anything on the Mac App Store, because the 30% fee by Apple is ridiculous. I do continue to sell my stuff outside the Mac App Store, and I think that this provides the better long-term experience for users. Now TableFlip is an experiment of sorts. Does the Mac App Store increase overall revenue for a “mainstream” app like this? I don’t think power user tools need to be on the Mac App Store, because power users are used to buy from gumroad, FastSpring, Paddle, etc. – But by putting TableFlip on the Mac App Store, I think I can reach more everyday Mac users who don’t know much about the indie developer ecosystem that prospers outside the warehouse that is the Mac App Store.

I’ll observe what happens. I don’t expect a net loss by this move, so that’s a good starting point. The new Mojave Mac App Store raised my hopes a bit. Maybe it’ll get a stronger editorial voice as well, like its iOS cousin. The Mac is not dead.

Finding the Field Editor in UI Tests

On macOS, the field editor is a NSText instance that appears whenever you edit a NSTextField. This means the text fields themselves offer no editing features; they just tell the shared field editor to appear in their drawing area and show their content.

When you write XCUITests, you may want to edit cells in a table or fill out a form with many text fields. Today I learned that you don’t get to the field editor in UI tests and send it the typeText message. You work with the text fields like the user does: as if they themselves accepted user input.

So there’s no reason to filter XCUIApplication().textViews and search for the field editor. Stick to textFields.element(matching: NSPredicate(format: "hasKeyboardFocus == true")) and you’re good. Note that it’s not hasFocus. The XCUIElementAttributes doc seems to suggest the element objects don’t respond to much more than what’s listed here, but that’s not the case.

let fieldEditor = XCUIApplication()
    .textFields
    .element(matching: 
        NSPredicate(format: "hasKeyboardFocus == true"))

I found this especially puzzling when editing NSTableCellView contents. Then again, the cell labels are NSTextFields, but with all the drawing attributes of a label. Think different.

TL;DR: There’s no point in searching for the field editor. All XCUIElementSnapshot objects that you can get to will not respond to NSText.isFieldEditor anyway. Just look for a text field that has focus.


→ Blog Archive