Alfred からポモドーロプロパティを追加 : Notion 解説(29)

(後日追加) ここで説明している Notion workflow はこちらで配布しています。 github.com

1. はじめに

私も、Yuka さんが紹介されたポモドーロトラッカー(Notionでポモドーロトラッカー🍅 #まいにちNotion Ep.10 - YouTube)を使っています。平日4日は「もくもくタイム」で、グループみんなで集中して作業を実施しています。もくもくタイムにおけるポモドーロの作業記録は、タスクとして記録できるようになっています。

f:id:hkob:20210710153443p:plain
ポモドーロトラッカー

タスクが入ると、「未スケジュール」に入り、ドラッグ&ドロップでポモの設定ができるようになっています。ポモドーロで実施しないタスクは「未🍅?」フラグをチェックすることで、ポモ以外の今日のタスクに移動します。

Notion を開いていれば、簡単に設定できるのですが、別の作業をしている時に Notion をわざわざ開くのも面倒だなということで、Alfred からポモドーロを設定できるようにしました。

2. Workflow の統合

2.1 フローの移動

これまで 3 つほど workflow を個別に作ってきました。説明を簡単にするために分けていましたが、今後も色々と Notion 関係で作りそうなので、Notion tools という一つのパッケージにまとめることにします。

f:id:hkob:20210710174209p:plain
Workflow の統合

まだ移動しただけなので、問題なく動作しています。このうち二つの Ruby スクリプトはいくつかの共通な部品が存在しています。これを Workflow の共通ライブラリに移動してしまうことにします。

2.2 共通部分の括りだし

まず、Workflow の存在するフォルダを開きます。このフォルダに notion.rb というファイルを作成します。

f:id:hkob:20210710174109p:plain
Workflow フォルダ

notion.rb の中身はこんな感じです。共通する post, patch の呼び出しを共通化しています。面倒な部分を全部ここにまとめてしまったので、本体は、本当に書きたいことだけを記述すればよいことになります。

require "net/http"
require "uri"
require "json"
require "date"

MY_NOTION_TOKEN=ENV["MY_NOTION_TOKEN"]
DATABASE_ID=ENV["DATABASE_ID"]
TITLE=ENV["TITLE"]
TZ="+09:00"

# post 専用のメソッド
def post_notion(uri, payload)
  request = Net::HTTP::Post.new(uri)
  request.content_type = "application/json"
  request["Authorization"] = "Bearer #{MY_NOTION_TOKEN}"
  request["Notion-Version"] = "2021-05-13"
  request.body = JSON.dump(payload)
  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
    http.request(request)
  end
  code = response.code
  unless code == "200"
    STDERR.print "Error!\n#{response.body}\n" # エラーの時だけエラーを表示する
  end
  JSON.parse(response.body)["results"]
end

def create_task(title, datetime)
  post_notion URI.parse("https://api.notion.com/v1/pages"), {
    "parent" => {
      "database_id" => "#{DATABASE_ID}"
    },
    "properties" => {
      "タスク名" => {
        "title" => [
          {
            "text" => {
              "content" => "#{title}"
            }
          }
        ]
      },
      "日付" => {
        "type" => "date",
        "date" => {
          "start" => "#{datetime}"
        }
      }
    }
  }
end

# ページの取得
def get_notion_pages(payload)
  post_notion URI.parse("https://api.notion.com/v1/databases/#{DATABASE_ID}/query"), payload
end

# patch 専用のメソッド
def patch_notion(uri, payload)
  request = Net::HTTP::Patch.new(uri)
  request.content_type = "application/json"
  request["Authorization"] = "Bearer #{MY_NOTION_TOKEN}"
  request["Notion-Version"] = "2021-05-13"
  request.body = JSON.dump(payload)
  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
    http.request(request)
  end
  code = response.code
  unless code == "200"
    STDERR.print "Error!\n#{response.body}\n" # エラーの時だけエラーを表示する
  end
end

# block 要素を追加するメソッド
def append_block_children(id, payload)
  patch_notion URI.parse("https://api.notion.com/v1/blocks/#{id}/children"), payload
end

# page プロパティを更新するメソッド
def update_page(id, payload)
  patch_notion URI.parse("https://api.notion.com/v1/pages/#{id}"), payload
end

2.3 Add Notion スクリプトの修正

Add notion はこんな感じで短くなりました。

require './notion.rb'

date = nil
summary = nil
time = nil
query = ARGV[0]
words = query.split " "
# 時間が存在するか?
time = words.pop if words[-1] =~ /\d+:\d+/
# time が nil でなければ timezone を追加
time += TZ unless time.nil?
begin
  date = Date.parse words[-1]
  # resque されてないなら words から抜いておく
  words.pop
rescue
end
# 日付がなければ今日にする
datetime = [(date || Date.today), time].compact.join " "
summary = words.join " "
create_task(summary, datetime)

2.4 Add Relation スクリプトの修正

Add Relation も共通部分が消えました。

require "./notion.rb"

# page_id を取得
pages = get_notion_pages({
  filter: {
    and: [
      {
        property: "タスク名",
        text: {
          starts_with: TITLE,
        },
      },
      {
        property: "日付",
        date: {
          equals: Date.today,
        },
      },
    ],
  },
})
id = pages.first["id"]

# 箇条書きを追加
append_block_children(id, {
  children: [
    {
      object: "block",
      type: "bulleted_list_item",
      bulleted_list_item: {
        text: [
          {
            type: "text",
            text: {
              content: "{query}",
            },
            annotaions: {
              bold: false,
              italic: false,
              strikethrough: false,
              underline: false,
              code: false,
              colod: "default",
            },
            plain_text: "{query}",
          }
        ],
      }
    }
  ]
})

3. ポモドーロプロパティ設定

前置きが長くなりましたが、ここからが本番です。

3.1 ポモドーロプロパティ設定の仕様策定

仕様はこんな感じで作成しました。

  • キーワードは「pn」(pomonodo notion の略)とします。引数はありません。
  • 日付が今日である「非🍅?」以外のタスクを取得します。
  • 「Done」フラグがついているものも含め、最大のポモドーロを取得します。
  • 「Done」フラグがついていないものだけを抽出し、ポモドーロの小さい順に並べ、一覧表示します。
  • 一覧から「Shift」を押しながら選択したら、タスクにDone を設定します。
  • 一覧から普通に選択したら、最大ポモドーロ以降の番号を並べて一覧表示します。
  • 表示されたポモドーロを選択したら、タスクのポモドーロを設定します。

3.2 フローの設置

フローはこんな感じになりました。以下、それぞれについて説明します。

f:id:hkob:20210710195923p:plain
作成したフロー

3.3 最初のスクリプトフィルタ(pn)

最初のスクリプトフィルタはこんな感じになっています。キーワードを「pn」、引数は要らないので「No Argument」に設定しています。

f:id:hkob:20210710201024p:plain
Script Filter (1)

スクリプトはこんな感じになっています。「非🍅?」以外の今日のタスクを取得します。今日のタスクの最大ポモドーロを取得したのち、終了したもの(「Done」が true のもの)を除外します。その後、items をキーに持つ json を表示すると、Alfred の一覧に表示されます。

require './notion.rb'

# page を取得し、利用しやすい Hash に変換
pages = get_notion_pages({
  filter: {
    and: [
      {
        property: "非🍅?",
        checkbox: {
          equals: false,
        },
      },
      {
        property: "日付",
        date: {
          equals: Date.today,
        },
      },
    ],
  },
  sorts: [
    {
      "property": "🍅",
      "direction": "ascending"
    }
  ],
}).map { |p|
  prs = p["properties"]
  {
    id: p["id"],
    title: (prs["タスク名"]["title"][0]["plain_text"] || ""),
    done: (prs["Done"]["checkbox"]),
    pomos: (prs["🍅"]["multi_select"].map { |ms| ms["name"] }),
  }
}

# 設定されている最後のポモドーロ番号を取得
max_pomo = pages.map { |p| p[:pomos] }.flatten.max

# 終了したページは除外
pages.reject! { |p| p[:done] }

items = {
  items: pages.map do |p|
    {
      # タイトル
      title: p[:title],
      # サブタイトルにポモドーロを描画
      subtitle: p[:pomos].join(", "),
      # 次のモジュールに渡す引数(id, title, 最大ポモ, 設定済ポモ)
      arg: [p[:id], p[:title], max_pomo, p[:pomos]].flatten.join("\t")
    }
  end
}
print JSON.dump(items)

ここまで実装すると、こんな画面が表示されます。サブタイトルの部分には、すでに設定済のポモドーロが表示されています。

f:id:hkob:20210710204921p:plain
Script Filter (1) の出力

3.4 Done の設定

簡単なので、シフトを押しながら選択して実行されるタスク終了処理を説明します。下に接続している線をクリックすると、「Action Modifier」の設定画面になります。ここで、「Shift」にチェックを入れ、押された時に「Finish the task」という文字列が表示されるようにします。

f:id:hkob:20210710210446p:plain
Action Modifier の設定

pn の後で、シフトキーを押すと、サブタイトルの部分が「Finish the task」に変わっていることがわかります。

f:id:hkob:20210710211336p:plain
Shift キーを押したときの表示

この先に繋がっている「Run Script」の内容はこんな感じです。Script Filter の arg にタブ区切りテキストが渡されているので、先頭の id を取得し、プロパティの「Done」を true に設定するだけです。共通部分を括りだしたので、簡潔に書けています。

require "./notion.rb"

id = ARGV[0].split("\t").first

update_page(id, {
  properties: {
    "Done": {
      checkbox: true
    }
  }
})

実行すると、task1 の「Done」フラグが付きました。

f:id:hkob:20210710213243p:plain
設定された「Done」フラグ

3.5 引数を退避

次に本道のポモドーロの設定に戻ります。次に再度 Script Filter を表示したいのですが、最初の引数をそのまま受け渡すと id などが見えてしまって、見た目が悪いです。そこで、Utility の「Arg and Vars」で引数を環境変数に退避しておきます。

設定画面は以下の通りです。Argument を空白にすることで、次の Script Filter には何も表示されなくなります。一方で、Script Filter からの出力は args という環境変数に登録されます。

f:id:hkob:20210710214130p:plain
Arg and Vars の設定

3.6 ポモドーロ選択表示

二番目のスクリプトはこんな感じになります。引数は消しているので、「No Argument」に設定しています。一つ目の Script Filter からのデータは環境変数 args に入っているので、それを使って処理をします。

f:id:hkob:20210710215623p:plain
Script Filter (2)

スクリプトは以下のようになりました。args にタブ区切りテキストを入れているので、それを展開してポモドーロの候補を作成します。最初に max_pomo よりも前のものを後ろ側に回し、その後設定済のポモドーロを除外します。一覧には、タイトルとしてポモドーロ名、サブタイトルとしてタスク名と設定済のポモドーロが表示されます。また、最後の Run Script に向けて、arg にタスクの id と設定したいポモドーロおよび設定済のポモドーロを渡します。

require "./notion.rb"

args = ENV["args"].split "\t"
task_id, title, max_pomo = args[0..2]
settled = args.length > 3 ? args[3..] : []

pomos = (1..5).map { |x| (1..4).map { |y| "#{x}-#{y}" } }.flatten
if (num = pomos.index max_pomo)
  pomos = [pomos[num..], pomos[0...num]].flatten
end
pomo_list = pomos - settled

items = {
  items: pomo_list.map do |p|
    {
      title: p,
      subtitle: "#{title}: #{settled.join(",")}",
      arg: [task_id, p, settled].flatten
    }
  end
}
print JSON.dump(items)

この結果、ポモドーロ選択肢はこんな感じで表示されます。task2 には 1-2, 1-3 が設定済のため、最大値1-3 以降の最初の候補である「1-4」が一番上に表示されています。

f:id:hkob:20210710222316p:plain
ポモドーロ選択肢の表示

3.7 ポモドーロ設定スクリプト

最後のスクリプトは非常に簡単です。並びが綺麗になるようにポモドーロは既存のものも合わせて、並び替えをして設定します。

require "./notion.rb"

id = ARGV[0]
pomos = ARGV[1..].sort

update_page(id, {
  properties: {
    "🍅": {
      multi_select: pomos.map { |p| {name: p } }
    }
  }
})

4. テスト実行

作成した Workflow をテストしてみました。今回は Twitter に動画を上げたので、それをリンクしておきます。

5. おわりに

今回は大掛かりになりましたが、思い通りのものが作れました。notion.rb に共通部分を括り出せたので、各スクリプトがかなりコンパクトになってわかりやすくなりました。今回の一番のどハマりした点は「ポモ」という属性が Alfred 側で取得できなかった点でした。例の濁点問題かと気づき、Notion の属性名を「ポモ」から「🍅」にした結果、スクリプトが動作するようになりました。


はてなブログに書いた Notion 記事一覧