Helping myself write more in the new year

Happy New Year! I’m implementing a Rulin’Blog post on this to come, I hope. this year to write something every day, and publish it here. It’s something I did for part of last year, but then I decided to change up my website, I got busy, and I fell way off. So here I am, renewed in my quest to post up an essay, poem, recipe, review, or something else every single day this year.

To help myself be more writerly, I decided to write out a script to automate the basic task of beginning a new blog post. I’ve done this before, but new blog, new script, so I began againPlus, I don’t really know where I put the dang original script.. I decided to try writing it in Python to get practice.To jump to the script itself, click here.

The dark times

My usual workflow is this:

  1. I type vim ~/acdw.net/posts/2019-01-03-the-title.essay in my shell.

  2. I manually type in the YAML frontmatter:

  3. I write up my post.

    Usually in writing up my post, I decide I want to change my title. So I change the title in the YAML block … but wait! The filename is wrong now too!

  4. So I have to
    • :wq from vim
    • run mv ~/acdw.net/posts/2019-01-03-the-title.essay ~/acdw.net/posts/2019-01-03-the-new-title.essay
    • vim ~/acdw.net/posts/2019-01-03-the-new-title.essay
    • oh my god.
  5. Rinse, repeat for who knows how many times, and for every post.

This is untenable.

Beginning again

I decided to implement a draft system in HakyllI did something very similar to Jorge Israel Peña’s method as outlined in his blog post., so I can put posts I’m working on in a drafts/ subfolder of my site directory and they won’t be published until they’re ready. But I thought, I can do one better — let’s work on a temporary file!

Python has a library for thatIs this a meme?, called tempfile. You can create a temporary file using tempfile.mkstemp that sticks around for a while, so I used that. It can also add the suffix .md, so vim knows it’s a markdown file, and a prefix so the user can know what they’re working on at a glance.

I just pop that into a function, and I’m good to go.

Frontmatter

Hakyll uses YAML frontmatter to define metadata about each postIt looks something like the YAML above., so my next step is to get that frontmatter in there automatically when I start the script. I just use a constant defined at the top of my scriptI use the {space} variable because I have Vim automatically automatically truncate end-of-line spaces on a save.:

and enter it with a quick

Editing

Okay, now comes the hard part: actually writing the post. I just use a subprocess.run for that, passing the $EDITOR variable from the environment (with a sane default, of course):

Saving

Remember, I’ve done all this as a tempfile. Now I need to actually save the file in the drafts/ folder. For that, I’ll need the date of the post, the slug, and the group, which is basically my version of a category. I just pass the group in as a parameter to new_post, so that’s taken care of. The date and slug need to be pulled from the YAML frontmatter of the file.

I used the re library instead of PyYAML, because pulling in a whole YAML dependency is silly for two fields and because PyYAML complained about multiple documents when I used the two ---s to delineate the metadataYou’re supposed to use --- for the beginning and ... for the end, but seriously, who cares? Hakyll doesn’t, I don’t, so there..

So I find the title and date by searching for their definitions in the file:

and then do a couple of sanity checks:

To generate the slug from the title, I just use my handy-dandy slugify function:

And then I write the file and remove the tempfile:

Quibbles

There are still a few problems. The first is how to tell my script what group the post should be in. I usually know what group I’m going to write in, but I’m not so sure where it’ll go from there, so that makes sense to pass as a script argument. Thus:

The second problem is that, sometimes I realize I jumped the gun. Like today, at first I was like, I’ll write a poem, then I decided to write this essay about writing essays. So I had to rm ~/acdw.net/drafts/2019-01-03-some-poem.poem so I didn’t clog up my drafts directory, which was very frustrating.

The solution is just to split the file’s contents along the ---s, then see if the body section is empty. If it is, you can bail:

Packaging

So my script is all done, but it’s a real pain to use. I set it up in a virtualenv, so if I want to use it I have to source the virtualenv and then run it with python and then deactivate the virtualenv. Luckily, we have bash for that! I wrote up a quick bash script that does all that for me and passes all the arguments to my python script and put it in my $PATH. You can see it below, after my python script.

Future thoughts

Even though my script is good enough to use today, I still want to add some features:

  • Check if you’re already working on something today, and ask if you’d like to continue on that or start something new
  • Maybe a draft command that allows you to choose a draft to work on
  • A publish command to move a draft into the posts/ folderI’m not sure whether this is a better command for this script or for my Hakyll site script. This requires more thought.
  • A kind of search function so I can quit and come back to a post without finding it in my drafts folder

I’ll work on these features later, though; too many times I’ve let tinkering get in the way of writing!

Appendix

The script

#!/usr/bin/env python3

import os
import re
import subprocess
import sys
import tempfile
from datetime import date

EDITOR = "editor"
DRAFT_DIR = os.getenv("HOME") + "/acdw.net/drafts/"
FRONTMATTER = """\
---
title:{space}
date: {date}
tags:{space}
---


""".format(
    space=' ', date=date.today())


def slugify(title):
    """Turn a title into a slug."""
    words = [str.lower(word) for word in re.split(r"\W+", title) if word != ""]
    return '-'.join(words)


def new_post(group, output_dir):
    # Make a new tempfile
    thandle, tname = tempfile.mkstemp(
        suffix='.md', prefix=f'acdw-{group}-', text=True)

    # Populate it with YAML frontmatter
    with open(thandle, mode='w') as f:
        f.write(FRONTMATTER)

    # Edit the file in user's EDITOR
    subprocess.run([os.getenv("EDITOR", EDITOR), tname, '+'], check=True)

    # Let's see the new file
    with open(tname, mode='r') as f:
        contents = f.read()

    _, metadata, body = contents.split('---')
    # If nothing has been written, bail
    if body.strip() == '':
        print("acdw: Post empty.")
        return None

    # Get metadata for filename
    title = re.search(r"^title:\s*(.*)$", metadata, re.MULTILINE)
    if title is not None:
        title = title.group(1)
    else:
        title = ""

    slug = slugify(title)

    date = re.search(r"^date:\s*(.*)$", metadata, re.MULTILINE)
    if date is not None:
        date = date.group(1)
    else:
        date = date.today()

    # Write the file to the directory
    fname = date + "-" + slug + "." + group

    with open(output_dir + fname, 'w+') as f:
        f.write(contents)

    # Delete tmp file
    os.remove(tname)

    print("acdw:", fname + " saved.")


if __name__ == "__main__":
    try:
        group = sys.argv[1]
    except IndexError:
        print("Usage: acdw <group>")

    new_post(group, DRAFT_DIR)

The wrapper