競プロとゲームをしていません

oj-prepareした問題をVSCodeとブラウザで開く【oj-prepare-open】

まずはこの動画を見てください。

youtu.be

私はoj-prepareで指定した問題やコンテストに対し、テンプレートをVSCodeで開き、ブラウザで問題ページを開くスクリプトを作成して使っています。

今回はこのスクリプト、名付けてoj-prepare-openを紹介します。

実装

fish-shellスクリプトです。

#!/bin/fish
# oj-prepare-open/hotarunx
# oj-prepareで指定した問題やコンテストに対し、
# テンプレートをVSCodeで開き、ブラウザで問題ページを開く

set templete_code_ext '.cpp' # 開くファイルの拡張子
set interval 60 # インターバル

set download_history ~/.cache/online-judge-tools/download-history.jsonl # ojの履歴ファイルのパス
set histories (tail -n100 $download_history) # 履歴ファイルのレコード最新100

set last_time (echo $histories[-1] | jq -r '.timestamp') # 最後の履歴のタイムスタンプ

# 履歴を走査する
for row in $histories
    # この履歴のタイムスタンプ
    set this_time (echo $row | jq -r '.timestamp')

    # この履歴と、最新の履歴の時間間隔がinterval未満のとき
    if test (math $last_time - $this_time) -le $interval
        # VSCodeで問題のテンプレートを開く
        find (echo $row | jq -r '.directory') -name '*'$templete_code_ext | xargs code
        # ブラウザで問題ページを開く
        open (echo $row | jq -r '.url')
    end
end

github.com

使用方法

使用前にjqをインストールしてください。 シェルはfish-shellを使用してください。 oj-prepare-open.shをダウンロードし、適当な場所に置いてください。

oj-prepareを実行した後、fish oj-prepare-open.shを実行してください。 過去最新のoj-prepareした問題やコンテストに対し、テンプレートをVSCodeで開き、ブラウザで問題ページを開きます。

*.cppファイル以外のテンプレートファイルを開きたいならば、次の行の拡張子を変更してください。

-set templete_code_ext '.cpp' # 開くファイルの拡張子
+set templete_code_ext '.py' # 開くファイルの拡張子

注意事項

  • fish-shellスクリプトなのでbash, zsh, powershell, cmd等々では動きません
  • 開くファイルの拡張子は1つしか指定できません
  • 最後にoj-prepareで指定した問題と、それより60秒前以内にoj-prepareで指定した問題に対してVSCodeとブラウザで開く処理になっています(実装の都合です)。60秒以内に複数回oj-prepareすると挙動がおかしくなります
  • 負荷軽減のため最新100の履歴しか参照しないようにしています。もし問題が100個以上あるコンテストに出るときは定数を変えてください

おまけ

VSCodeタスク連携

hotarunx.hatenablog.com この記事で解説したテンプレート生成とサンプルのダウンロードのタスクの後にoj-prepare-openを実行したいならば、次のように書きます。

        {
            "label": "oj-prepare: テンプレート生成とサンプルのダウンロード",
            "type": "shell",
            "command": "oj-prepare",
            "args": [
                "${input:URL}",
                ";",
                "fish",
                "oj-prepare-open.sh"
            ],
            "group": "test"
        },

実装方法

oj-prepare-openの実装方法について説明します。

まず、ojは以下のような内容の履歴ファイルを持ちます。

~/.cache/online-judge-tools/download-history.jsonl

{"timestamp": 1636281672, "directory": "/home/hotaru/procon/atcoder.jp/abs/practice_1/atcoder.jp/abs/practice_1", "url": "https://atcoder.jp/contests/abs/tasks/practice_1"}
{"timestamp": 1636281672, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc086_a/atcoder.jp/abs/abc086_a", "url": "https://atcoder.jp/contests/abs/tasks/abc086_a"}
{"timestamp": 1636281673, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc081_a/atcoder.jp/abs/abc081_a", "url": "https://atcoder.jp/contests/abs/tasks/abc081_a"}
{"timestamp": 1636281674, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc081_b/atcoder.jp/abs/abc081_b", "url": "https://atcoder.jp/contests/abs/tasks/abc081_b"}
{"timestamp": 1636281674, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc087_b/atcoder.jp/abs/abc087_b", "url": "https://atcoder.jp/contests/abs/tasks/abc087_b"}
{"timestamp": 1636281675, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc083_b/atcoder.jp/abs/abc083_b", "url": "https://atcoder.jp/contests/abs/tasks/abc083_b"}
{"timestamp": 1636281676, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc088_b/atcoder.jp/abs/abc088_b", "url": "https://atcoder.jp/contests/abs/tasks/abc088_b"}
{"timestamp": 1636281676, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc085_b/atcoder.jp/abs/abc085_b", "url": "https://atcoder.jp/contests/abs/tasks/abc085_b"}
{"timestamp": 1636281677, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc085_c/atcoder.jp/abs/abc085_c", "url": "https://atcoder.jp/contests/abs/tasks/abc085_c"}
{"timestamp": 1636281678, "directory": "/home/hotaru/procon/atcoder.jp/abs/arc065_a/atcoder.jp/abs/arc065_a", "url": "https://atcoder.jp/contests/abs/tasks/arc065_a"}
{"timestamp": 1636281679, "directory": "/home/hotaru/procon/atcoder.jp/abs/arc089_a/atcoder.jp/abs/arc089_a", "url": "https://atcoder.jp/contests/abs/tasks/arc089_a"}
{"timestamp": 1636282092, "directory": "/home/hotaru/procon/atcoder.jp/abs/practice_1", "url": "https://atcoder.jp/contests/abs/tasks/practice_1"}
{"timestamp": 1636282092, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc086_a", "url": "https://atcoder.jp/contests/abs/tasks/abc086_a"}
{"timestamp": 1636282092, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc081_a", "url": "https://atcoder.jp/contests/abs/tasks/abc081_a"}
{"timestamp": 1636282093, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc081_b", "url": "https://atcoder.jp/contests/abs/tasks/abc081_b"}
{"timestamp": 1636282094, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc087_b", "url": "https://atcoder.jp/contests/abs/tasks/abc087_b"}
{"timestamp": 1636282094, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc083_b", "url": "https://atcoder.jp/contests/abs/tasks/abc083_b"}
{"timestamp": 1636282095, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc088_b", "url": "https://atcoder.jp/contests/abs/tasks/abc088_b"}
{"timestamp": 1636282096, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc085_b", "url": "https://atcoder.jp/contests/abs/tasks/abc085_b"}
{"timestamp": 1636282097, "directory": "/home/hotaru/procon/atcoder.jp/abs/abc085_c", "url": "https://atcoder.jp/contests/abs/tasks/abc085_c"}
{"timestamp": 1636282098, "directory": "/home/hotaru/procon/atcoder.jp/abs/arc065_a", "url": "https://atcoder.jp/contests/abs/tasks/arc065_a"}
{"timestamp": 1636282099, "directory": "/home/hotaru/procon/atcoder.jp/abs/arc089_a", "url": "https://atcoder.jp/contests/abs/tasks/arc089_a"}
{"timestamp": 1636286446, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_a", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_a"}
{"timestamp": 1636286447, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_b", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_b"}
{"timestamp": 1636286447, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_c", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_c"}
{"timestamp": 1636286448, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_d", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_d"}
{"timestamp": 1636286449, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_e", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_e"}
{"timestamp": 1636286450, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_f", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_f"}
{"timestamp": 1636286450, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_g", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_g"}
{"timestamp": 1636286451, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_h", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_h"}
{"timestamp": 1636790564, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_a", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_a"}
{"timestamp": 1636790564, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_b", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_b"}
{"timestamp": 1636790565, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_c", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_c"}
{"timestamp": 1636790566, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_d", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_d"}
{"timestamp": 1636790566, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_e", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_e"}
{"timestamp": 1636790567, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_f", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_f"}
{"timestamp": 1636790567, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_g", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_g"}
{"timestamp": 1636790568, "directory": "/home/hotaru/procon/atcoder.jp/abc226/abc226_h", "url": "https://atcoder.jp/contests/abc226/tasks/abc226_h"}
{"timestamp": 1636790683, "directory": "/home/hotaru/procon/atcoder.jp/agc055/agc055_a", "url": "https://atcoder.jp/contests/agc055/tasks/agc055_a"}
{"timestamp": 1636790683, "directory": "/home/hotaru/procon/atcoder.jp/agc055/agc055_b", "url": "https://atcoder.jp/contests/agc055/tasks/agc055_b"}
{"timestamp": 1636790684, "directory": "/home/hotaru/procon/atcoder.jp/agc055/agc055_c", "url": "https://atcoder.jp/contests/agc055/tasks/agc055_c"}
{"timestamp": 1636790684, "directory": "/home/hotaru/procon/atcoder.jp/agc055/agc055_d", "url": "https://atcoder.jp/contests/agc055/tasks/agc055_d"}
{"timestamp": 1636790685, "directory": "/home/hotaru/procon/atcoder.jp/agc055/agc055_e", "url": "https://atcoder.jp/contests/agc055/tasks/agc055_e"}
{"timestamp": 1636790686, "directory": "/home/hotaru/procon/atcoder.jp/agc055/agc055_f", "url": "https://atcoder.jp/contests/agc055/tasks/agc055_f"}

このファイルに、oj-prepareで指定した問題のディレクトリとURLがJSONL形式で書いてあります。 そこで、Linux向けJSON操作コマンドであるjqでこのファイルをパースします。 そうしてcodeでテンプレートをVSCodeで開き、openコマンドでブラウザで開きます。

さて、1回のoj-prepareコマンドで複数の問題を指定可能です。 しかし、この履歴ファイルでは、ある時実行したoj-prepareコマンドでどの問題を指定したのかわかりません。 oj-prepare-openでは過去最新のoj-prepareした問題やコンテストに対し、テンプレートをVSCodeで開き、ブラウザで問題ページを開きたいです。困りました。 そこで最後の履歴から、60秒前までの履歴が、過去最新のoj-prepareした問題という仮定を置きました。