Skip.life

Why read it later when you can skip it for life?

Skip.life — The “Read it Later” service for people too busy to Read It Later

I enjoy a good, steamy hot take as much as anyone, I just don’t always have the time right now. That’s why I use Instapaper. Or, used to use.

Because my Instapaper reading list grew faster than I would read whatever I would put in it, every visit became more about feeling guilty than enjoying taking the ;dr part out of the tl blog posts.

So I moved to Pocket.

And what a relief! Just me and a completely fresh Pocket archive, empty of articles and guilt. Ready to be filled with longreads of whatever we are very, very concerned about this week.

And so I quickly began throwing anything in there. I even added my friends as friends and added a few of their recommendations. Some of them had many and that made them seem just the kind of smart that I want to be.

Until a few days later when my Pocket started to look like my Instapaper. Uselessly filled with a ambitions I couldn’t live up to. So I ditched it and began using Safari’s built-in Reading List instead. And what a relief?

I was beginning to see a pattern and needed a drastic change. So I made skip.life:

Skip.life integrates 100% with your browser

Introducing skip.life

Skip.life is a browser extension, an app and a way of life. Just like you’d expect from Instapaper, Pocket or the other guilt trippers. Come across an article you’d love to read *just not right now? *Click the “Save” Button™ and you will be treated to a very satisfying animation of a robot arm carefully archiving the article for later enjoyment, then the tab will close.

Yet, nothing else will happen.

Skip.life will skip it for you.

You’ll feel satisfied that you didn’t miss out on the article. You definitely saved it for later. And 6 minutes later you will have forgotten both the article and every intention of reading it — and best of all, you’ll feel zero anxiety when you log into skip.life and see that you have 0 unread articles.

You must be very smart!

Getting a project's Ruby version from the Gemfile

It used to be that every project had its own .ruby_version file. But you’d also have to specify the same version in the Gemfile. I say nay to .ruby_version and instead get it from one place:

alias ruby-vers="cat Gemfile | grep '^ruby' | sed -E \"s/.*[\\\"'](.+)[\\\"']/\1/"\"

Now you can $ chruby ruby-vers your way to success!

Using Preact in production with Rails' Webpacker

Preact is like React but smaller in size. I think? Anyway, so I’ve been told. It’s also different in some ways and I’m not entirely sure how, but they have this thing called preact-compat that’ll plug right into your Webpack config and use Preact instead of React which seems like free filesize savings. Right on!

First:

$ yarn add preact-compat webpack-merge

In config/webpack/production.js:

const environment = require("./environment");
const merge = require("webpack-merge");

module.exports = merge(environment.toWebpackConfig(), {
  resolve: {
    alias: {
      react: "preact-compat",
      "react-dom": "preact-compat",
    },
  },
});

I saved 2 whole megabytes with just this change. No, of course that’s not true.

Disable long-press for Hyper on macOS

Hyper is great and even though I still cling to iTerm I sometimes try and shake things up by using Hyper for a few days.

macOS has a feature where if you press a character like u and hold it, a popup menu appears that’ll let you pick out a ü instead. Great if you’re writing German prose. Not great if you are undoing a whole day’s work in vim.

So let’s disable that feature only for Hyper. Put this in your term and smoke it:

defaults write co.zeit.hyper ApplePressAndHoldEnabled -bool false

If you want to disable it system wide then use the same command but without the co.zeit.hyper part.

Conditionally load local plugs in vim

If you’re using vim (you should) and vim-plug for plugins (you should) and you have some plugins locally (you might) but still want your setup to be portable (you should) then you might want to only load plugins locally if they exist (here’s how).

function! s:maybeLocalPlug(args)
  let l:localPath = $HOME . "/dev/" . expand(a:args)

  if isdirectory(l:localPath)
    Plug l:localPath
  else
    Plug 'mikker/' . expand(a:args)
  endif
endfunction

call s:maybeLocalPlug('lightline-theme-pencil')
call s:maybeLocalPlug('vim-rerunner')
call s:maybeLocalPlug('vim-dimcil')
call s:maybeLocalPlug('vim-colors-paramount')

This checks to see if the plugin exists locally at ~/dev/plugin-name and if it doesn’t it loads from good old Github.

Sprinkling React

A beautiful thing about jQuery was how you could sprinkle it on top of an already working site. With all of today’s SPAs and what nots this seems like a distant past. We’ve all drunk the Kool Aid and the benefits of React (and the like) are just too huge to ever look back.

Still, there’s no reason we can’t still sprinkle instead SPA-ing. That was a horrible sentence. Let’s move on.

Here’s what I do on 10er.dk.

The api is basically as follows. You have a div with some data- props that we’ll catch later and switch in the magic.

<article>
  <div data-react="NumberWang" data-props='{"numbers":[1,2,3,5,68]}'></div>
</article>

Ok, so that means look up the component called NumberWang, render it with these props and throw it back in here.

Here’s sprinkleComponents.js. It uses Webpack and its require.context:

import React from "react";
import { render } from "react-dom";

let context = require.context("./components", true, /\.(\/index)?js$/);

export default function sprinkleComponents() {
  const nodes = document.querySelectorAll("[data-react]");

  for (const node of nodes) {
    const regexp = new RegExp(node.dataset.react + "(/index)?\\.js");
    const match = context.keys().find((path) => path.match(regexp));
    const Comp = context(match).default;
    const props = node.dataset.reactProps
      ? JSON.parse(node.dataset.reactProps)
      : {};
    props.children = node.innerHTML;

    render(<Comp {...props} />, node);
  }
}

// We can even get hot module reload (so dx am I right?)
if (module.hot) {
  module.hot.accept(context.id, (upd) => {
    context = require.context("./components", true, /\.(\/index)?js$/);
    sprinkleComponents();
  });
}

This expects to find the component NumberWang at either ./components/NumberWang.js or ./components/NumberWang/index.js relative to sprinkleComponents.js.

Let’s make it – ./components/NumberWang.js:

import React from "react";

export default ({ numbers }) => (
  <div>{numbers[Math.floor(Math.random() * numbers.length)]}</div>
);

And finally in your main.js or index.js or client.js or whatever:

document.addEventListener("DOMContentLoaded", () => {
  sprinkleComponents();
});

This is the technique that I use to selectively apply some js sauce on 10er.dk, a mostly server rendered Rails app.

Serving a Keybase.io claim with Next

I recently moved my personal website to next.js because why not. The people at ▲ Zeit are on a roll and I want in on it.

Keybase quickly started complaining about me removing my claim. You’re supposed to host a file at /keybase.txt with specific content to claim a domain is truly yours. So how do we do that with Next?

Here’s pages/keybase.txt.js:

import React, { Component } from "react";

export default class KeybaseTxt extends Component {
  static getInitialProps({ res }) {
    res.end(claim);
  }

  render() {
    return <div>We'll never get this far</div>;
  }
}

const claim = `=====...long long claim thing...`;

getInitialProps is Next’s way of fetching data before render so it works both on the server and the client. During server render it’s passed req and res from node and apparently we can just end it right there. So dumb it’s smart.

Control-h in NeoVim on macOS

Apparently <c-h> sends <BS> to xterm256-screen terminals and your binding doesn’t work in Neovim.

There’s a few fixes, but I like this one that does it in iTerm:

  1. Edit -> Preferences -> Keys
  2. Press +
  3. Press Ctrl+h as Keyboard Shortcut
  4. Choose Send Escape Sequence as Action
  5. Type [104;5u for Esc+

Read marks with Ecto and Phoenix

As always, Rails has an easy, pluggable way to do this with unread but it isn’t too hard to add the same to your Phoenix app. Let’s try!

First we’ll generate the table and schema that holds the read marks. This assumes you have two tables: users and stories.

mix phoenix.gen.model ReadMark read_marks \
  user_id:references:users \
  story_id:references:stories

Open up the newly generated web/models/read_mark.ex and set up the associations:

defmodule MyApp.ReadMark do
  use MyApp.Web, :model

  schema "read_marks" do
    belongs_to :user, MyApp.User
    belongs_to :story, MyApp.Story

    timestamps()
  end
end

And in the existing models:

defmodule MyApp.User do
  schema "users" do
    has_many :read_marks, MyApp.ReadMark, on_delete: :delete_all
  end
end

defmodule MyApp.Story do
  schema "stories" do
    has_many :read_marks, MyApp.ReadMark, on_delete: :delete_all
  end
end

This is all the setup we need.

We can now get unread stories for a user with a query like:

defmodule MyApp.Story do
  #...

  def unread_by(query, user) do
    from s in query,
      left_join: rm in assoc(s, :read_marks),
      on: rm.user_id == ^user.id,
      where: is_nil(rm.id)
  end
  def unread_by(user), do: unread_by(DRBot.Story, user)
end

And use it like:

user = Repo.get(MyApp.User, 1)
unread_stories = MyApp.Story.unread_by(user) |> Repo.all

We can even chain it with other queries:

query = from s in MyApp.Story, where: s.published_on == ^today

unread_stories_published_today =
  MyApp.Story.unread_by(query, user)
  |> Repo.all

# or the other way around

query = MyApp.Story.unread_by(user)

unread_stories_published_today =
  (from s in query, where: s.published_on == ^today)
  |> Repo.all

Podcast Mixtapes

Here’s an idea for a web thing that you should make and afterwards figure out how to make money from. Bonus points if it doesn’t include VC or ads. (Minus points if both.)

One day, sitting at work, you laugh out loud because Greg the host of your favourite podcast “Tim & Greg’s Lazy Hour” says he’s been biking during the weekend and, well, if one knows Greg like you do, that would be what Greg would have been up to, so you simply cannot hold it back. You crack open with a half spitting / half laughing noise.

Seconds later coming back to reality you look up and notice your coworker looking at you, tainted by your spit, hinting at you to take off your headphones.

“What are you listening to?” she asks. Oh, where to begin.

Podcasts come in feeds. This is one of the best things about them. Over time you build a relationship with each one. You are there when all the internal jokes come to life and by now you feel like you and the hosts should just rent a cabin some time and be best pals because it feels like you already are.

But you can’t instruct your coworker to listen to the prior 136 episodes and then she’ll know. You have to point her to something that will ease her into it.

podcastmixtapes.sexy

Luckily there’s podcastmixtapes.sexy (not a real thing. Yet.)

  1. Coworker wants in on the funnies.
  2. You create a mixtape, pick the 4–5 episodes that’ll give her the best intro, give it the title “Up to speed on Tim & Greg”.
  3. After the first episode there are a few things that you should probably explain so you type in a few curator’s notes. These get added to the feed in between the episodes.
  4. You share the address of the mixtape with coworker.
  5. Coworker subscribes to the mixtape with her podcast player (like a decent person) or listens to it directly in her browser (like an animal.)

Your coworker is now better suited to either subscribe to the real deal - or judge you and your weird ass humour.

Other mixtape ideas:

  • Episodes that’ve made me cry in public
  • Intro to Roderick on the Line and the #supertrain movement
  • Best of This American Life pre episode 500
  • You Look Nice Today episodes mentioning Adam’s drumming
  • For Lisa ❤️🎧 (ongoing, made by Lisa’s boyfriend)
  • Siracusa Setting Things Straight (3+ new episodes each week)

Now go seize this fortune that I’ve left in your hands.