mattn/memoでさらに快適にメモを書くためにやっている設定について

公開用のテキストはブログで書くことが多いですが、公開できない話や落書きのような内容はローカルのテキストファイルに書き散らしているという人は多いと思います。私もそういう活用をしていて、今年からmattn/memoを使っています。

mattn/memoはGoで書かれていて、800行程度なのですぐに読めますし、気になる挙動があったらしゅっと直せます(自分も細かいPull Requestをいくつか送ってマージしてもらった)。特に以下の点が気にいっています。

  • Goで書かれていて、高速に立ち上がる
  • 簡単にgrepしたり(memo g)、pecoでファイル選択(memo e)ができる
  • httpサーバーが立ち上がって(memo s)、markdownがいい感じに見れる
    • テンプレートファイルをいじると自分の好きなようにできる(後述)

半年ほど使って、メモも大分書いたので、より使い勝手を増すために自分がやっている設定を書いてみようと思います(ディフォルトの設定で使っていても十分使いやすいとは思います)。

Emacsからさっと開く

memo nから新しいテキストファイルを作れますが、同じ日付けの事柄は1つのテキストファイルに日報として書いています。なので、その日の日報ファイルが3秒で開けることが大事です。EmacsからM-x memoですぐに開けるようにしておきます。

(defun memo ()
  (interactive)
  (find-file
   (concat "~/Dropbox/_posts/" (format-time-string "%Y-%m-%d") "-日報.md")))

vimの人も似たようなことやってそう。

数式の見栄えやコードのシンタックスハイライト

論文を読んだときのちょっとした数式のメモや、スニペット的にメモにコードを貼っておくということは結構あると思います。その数式が綺麗に描画されたり、スニペットがシンタックスハイライトされていると、大したことないものでもテンションが上がってきます(大事!)。mattn/memoではテンプレートファイルを自分で自由に書けるので、私は数式描画にはKaTeXを、シンタックスハイライトにはhighlight.jsを使っています。cdnから読み込んでもいいですが、ダウンロードしてローカルに置いておくと、ネットに繋らない環境でも全く問題なく使えるので、そちらがよいでしょう。特にKaTeXは描画も相当早いし、aligned環境も使えてなんだこりゃ…という感じでした。

f:id:syou6162:20170520174939p:plain

そんなに凝ってないLaTeXだったら、ほぼ変えずにいい感じにmarkdownで書ける。markdownで雑に書いておいて、LaTeXに持っていくこともできそう。

f:id:syou6162:20170520174703p:plain

tableの見た目などを整えるためにlessが書きたかったので、less.jsも使っています。

テンプレートファイルの内容

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>{{.Name}}</title>

    <link rel="stylesheet/less" type="text/css" href="/assets/style.less">
    <script src="/assets/less.js" type="text/javascript"></script>

    <link rel="stylesheet" type="text/css" href="/assets/atelier-dune-light.min.css">
    <script src="/assets/highlight.min.js" type="text/javascript"></script>

    <link rel="stylesheet" type="text/css" href="/assets/katex.min.css">
    <script src="/assets/katex.min.js" type="text/javascript"></script>

    <script src="/assets/auto-render.min.js"  type="text/javascript"></script>

    <script>
     document.addEventListener("DOMContentLoaded", () => {
       var katex_opts = {
         displayMode: true
       };

       Array.from(document.querySelectorAll("div"), elem => {
         if (elem.className === "highlight highlight-math") {
           var e = elem.querySelector("pre");
           katex.render(e.textContent, e, katex_opts);
         } else {
           hljs.highlightBlock(elem.querySelector("pre"));
         }
       });

       renderMathInElement(document.body, {
         delimiters: [
           {left: "$", right: "$", display: false},
         ]
       });
     });
    </script>
  </head>
  <body>{{.Body}}</body>
</html>

空のテキストファイルを削除するサブコマンド

mattn/memoではサブコマンドを簡単に実装することができます。私の場合、日報ファイルを開いたもののその日は力尽きて空ファイルができて放置されてしまっていたので、それらを掃除するサブコマンドを実装しました。Goの練習にちょうどいい感じで簡単に書ける。

サブコマンドのコード

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
    "regexp"
    "sort"
    "strings"

    "github.com/fatih/color"
    tty "github.com/mattn/go-tty"
)

const (
    memoDir            = "/Users/yasuhisa/Dropbox/_posts"
    emptyContentRegexp = `(?s)^# 日報[\n\s]*$`
)

type File struct {
    Path    string
    Content string
}

func filterMarkdown(files []string) []string {
    var newfiles []string
    for _, file := range files {
        if strings.HasSuffix(file, ".md") {
            newfiles = append(newfiles, file)
        }
    }
    sort.Sort(sort.Reverse(sort.StringSlice(newfiles)))
    return newfiles
}

func isEmptyContent(content string) bool {
    return regexp.MustCompile(emptyContentRegexp).MatchString(content)
}

func ask(prompt string) (bool, error) {
    fmt.Print(prompt + ": ")
    t, err := tty.Open()
    if err != nil {
        return false, err
    }
    defer t.Close()
    var r rune
    for r == 0 {
        r, err = t.ReadRune()
        if err != nil {
            return false, err
        }
    }
    fmt.Println()
    return r == 'y' || r == 'Y', nil
}

func deleteFile(f File) error {
    color.Red("%s", "Will delete the following entry. Are you sure?")
    fmt.Println("File: " + f.Path)
    fmt.Println(f.Content)
    answer, err := ask("Are you sure? (y/N)")
    if answer == false || err != nil {
        return err
    }
    answer, err = ask("Really? (y/N)")
    if answer == false || err != nil {
        return err
    }
    err = os.Remove(f.Path)
    if err != nil {
        return err
    }
    color.Yellow("Deleted: %v", f.Path)
    return nil
}

func main() {
    f, _ := os.Open(memoDir)
    defer f.Close()
    files, _ := f.Readdirnames(-1)
    files = filterMarkdown(files)
    for _, file := range files {
        path := filepath.Join(memoDir, file)
        b, _ := ioutil.ReadFile(path)
        content := string(b)
        if isEmptyContent(content) {
            deleteFile(File{path, content})
        }
    }
}

簡易markdown previewerとしてmemoを使う

~/.config/memo/config.tomlの設定項目の中のmemodirでmarkdownを置いているディレクトリを指定できます。しかし、たまにそのディレクトリ以外のREADME.mdなどのプレビューを見たいときがあります。そういうときはシンボリックリンクを貼ってごまかしながら使っています。

% ln -s ${PWD}/README.md ~/Dropbox/_posts/2017-05-20-hoge.md

これで、http://localhost:3456/の一覧にファイル(2017-05-20-hoge.md)が出てくるようになるので、html描画後の様子を見ながら編集します。いい感じに編集が終わったらunlinkで掃除。

% unlink ~/Dropbox/_posts/2017-05-20-hoge.md

supervisorでmemoをサーバーとして起動時に立ち上げる

memo s --addr :3456でhttpサーバーがさっと立ち上がるのですが、ずっと使っていると立ち上げるのすら面倒で、PC起動したら立ち上がっていてくれという気持ちになりました。そこで、supervisorでmemoをサーバーとして起動時に立ち上げるようにしました。/usr/local/share/supervisor/conf.d/memo.confにこんな感じに書いておく。

設定ファイルの内容

[program:memo]
command=zsh -c 'direnv exec . memo s --addr :3456'
directory=%(ENV_HOME)s
user=yasuhisa
numprocs=1
stdout_logfile=/tmp/memo_out.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=3
stderr_logfile=/tmp/memo_err.log
stderr_logfile_maxbytes=10MB
stderr_logfile_backups=3
autostart=true
autorestart=true

~/.config/memo/config.toml等を編集した場合、再起動しないと反映されないと思うので、適当にkillします。supervisorでautorestartがかかるので、ちょっと待っていると設定が反映されます。