Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

Claude CodeのstatuslineをGleamで実装した

Claude Codeのstatuslineという機能が少し前に実装され、Claude Code起動中に必要なステータスを表示しておけるようになりました。Claude Codeの提供するコンテキストウィンドウや利用料の表示のほか、たとえばgitブランチの表示といった機能を提供することができます。

JSONをパースできて、出力内容を標準出力できればstatuslineを表示できるようなので、標準出力をできる任意のプログラミング言語で実装できます。つまりShell ScriptでもPythonでもTypeScriptでも、Rustでも実装できるというわけです。なかなか遊びがいがありそうですね。

ちなみにですが、/statuslineをClaude Code上で実行するとプロンプトが立ち上がり、指示に従って設定をするだけでstatuslineの表示をカスタマイズすることもできます。むしろそちらの方が標準的な使い方でしょう。完全に最近AIに奪われていたコーディング欲を満たすための作業です。

Gleam

最近私が注目している言語にGleamというプログラミング言語があります。このプログラミング言語はRustフレーバーな関数型プログラミング言語で、比較的最近バージョン1.0がリリースされた注目を集めるプログラミング言語です。おもしろいのが、Erlang/OTPとJavaScriptの両方をターゲットとして成果物を出力できる点です。関数型プログラミングをしながらスクリプトを記述できるという点に惹かれて、最近ちょっとしたスクリプトを書く際、Gleamを使って書くようにしています。

推しポイントとして、文法機能のひとつにパイプライン演算子 |> があります。これを使うと、関数同士を結合させていくことができます。Gleam全体としてこの機能を利用する前提で標準ライブラリなどが設計されていて、普通にコードを書いていて利用します。便利です。

/// Reads a file and returns its contents as a list of lines
/// Returns an error message if the file cannot be read
fn read_file(path: String) -> Result(List(String), String) {
  simplifile.read(path)
  |> result.map(fn(content) { string.split(content, "\n") })
  |> result.map_error(fn(e) {
    string.inspect(e)
    |> string.append(": File Not Found")
  })
}

上記はパスを受け取ってファイルの中身を読み取り、内容を文字列のリストにして返すという実装です。まずsimplifile.read関数でファイルの中身を読み取り、読み取った結果がResult型として返ってきます。ファイルからの読み取りが成功だった場合、返ってきた結果を、改行ごとにスプリットしてリストに変えておきます。仮にファイルからの読み取りの結果がエラーだった場合、エラーの文字列を加工します。

このように、処理を行う関数を次々パイプライン演算子で繋げていき、パイプライン全体としてひとつの処理を行うよう組み上げていきます。パイプラインの左右ひとつひとつをモジュール化して細かくしながら実装するとうまくコードがワークしてくれるため、とりあえず関数に切り出して最後パイプラインで繋げてしまおうという実装の仕方に頭が切り替わります。またもちろん、パイプラインの左右における型の遷移もきっちり見られるため、最終的に出来上がるコードはとてもロバスト(堅牢)になります。

さてここで気になるのがコーディングエージェントの対応状況です。このようなまだ利用者数の少なそうなプログラミング言語を使って、コーディングエージェントはきちんと動くのかです。これについては、Claude Codeでは今のところ、とくに苦労することなくコード生成できています。むしろ今のところは、私より詳しいくらいです。一方で注意点として、正直なところ超小規模なプロジェクトをいくつかやっている程度なので、中規模〜大規模化した際にどうなるかは未知数だという点が挙げられます。

作ってみた

Gleamによるサンプル

github.com

プロンプトを入力する箇所の下にステータスラインを表示している

というわけでGleamで作ってみました。一旦statuslineの文字列をレンダリングする箇所だけ示すと、パイプライン演算子を組み合わせてJSONをパースした後、文字列の結合<>で結果をすべて結合しているだけです。その後、io.println関数で標準出力しています。以下は主要な処理を行う、コードの一部抜粋です。

pub fn main() {
  let input =
    stdin.read_lines()
    |> yielder.to_list
    |> string.join("\n")

  case json.parse(from: input, using: decode.dynamic) {
    Error(_) -> io.println("Error happened while parsing")
    Ok(value) -> render(value)
  }
}

type StatusLine {
  Statusline(
    model: String,
    used_percentage: Int,
    used_usd_cost: Float,
    input_tokens: Int,
    total_input_tokens: Int,
    output_tokens: Int,
    total_output_tokens: Int,
  )
}

fn build_status_line(root: Dynamic) -> StatusLine {
  let model =
    root
    |> decode_model
    |> result.unwrap("Claude")

  let pct =
    root
    |> decode_used_percentage
    |> result.unwrap(0)

  let usg =
    root
    |> decode_used_usd_cost
    |> result.unwrap(0.0)

  let input_tokens =
    root
    |> decode_input_tokens
    |> result.unwrap(0)

  let total_input_tokens =
    root
    |> decode_total_input_tokens
    |> result.unwrap(0)

  let output_tokens =
    root
    |> decode_output_tokens
    |> result.unwrap(0)

  let total_output_tokens =
    root
    |> decode_total_output_tokens
    |> result.unwrap(0)

  Statusline(
    model,
    pct,
    usg,
    input_tokens,
    total_input_tokens,
    output_tokens,
    total_output_tokens,
  )
}

fn render(root: Dynamic) -> Nil {
  let status = build_status_line(root)

  io.println(
    "🤖 "
    <> status.model
    <> " | 🧠 "
    <> int.to_string(status.used_percentage)
    <> "% | 🔥 \u{eab4} "
    <> format_tokens(status.input_tokens)
    <> "/"
    <> format_tokens(status.total_input_tokens)
    <> " \u{eab7} "
    <> format_tokens(status.output_tokens)
    <> "/"
    <> format_tokens(status.total_output_tokens)
    <> " | 💸 $"
    <> format_cost(status.used_usd_cost),
  )
}

この実装はClaude Codeの出力するJSONファイルを読み取って変換をしているだけですが、たとえばgitのブランチ名を表示するといった拡張方法もありそうです。これもGleamならパッと実装できそうで、欲しくなったタイミングで実装してみようかと思っています。

解析対象のJSON

結局のところ、やっている処理は標準入力にやってきたJSONの読み取りとちょっとした文字列の加工です。では、読み取り対象のJSONファイルはどのような構成になっているのでしょうか?サンプルファイルはClaude Codeのサイトに掲載されています。なお現時点では英語版が最新のドキュメントのようで、日本語の方は更新されておらずJSONが古いということがわかりました。

code.claude.com

{
  "cwd": "/current/working/directory",
  "session_id": "abc123...",
  "transcript_path": "/path/to/transcript.jsonl",
  "model": {
    "id": "claude-opus-4-6",
    "display_name": "Opus"
  },
  "workspace": {
    "current_dir": "/current/working/directory",
    "project_dir": "/original/project/directory"
  },
  "version": "1.0.80",
  "output_style": {
    "name": "default"
  },
  "cost": {
    "total_cost_usd": 0.01234,
    "total_duration_ms": 45000,
    "total_api_duration_ms": 2300,
    "total_lines_added": 156,
    "total_lines_removed": 23
  },
  "context_window": {
    "total_input_tokens": 15234,
    "total_output_tokens": 4521,
    "context_window_size": 200000,
    "used_percentage": 8,
    "remaining_percentage": 92,
    "current_usage": {
      "input_tokens": 8500,
      "output_tokens": 1200,
      "cache_creation_input_tokens": 5000,
      "cache_read_input_tokens": 2000
    }
  },
  "exceeds_200k_tokens": false,
  "vim": {
    "mode": "NORMAL"
  },
  "agent": {
    "name": "security-reviewer"
  }
}

JSONの中身をstdinで受け取って加工するのが今回のGleamの実装内容です。下記のようにして動作確認をすることもできます。

$ gleam run <<EOF
{
  "cwd": "/current/working/directory",
  "session_id": "abc123...",
  "transcript_path": "/path/to/transcript.jsonl",
  "model": {
    "id": "claude-opus-4-6",
    "display_name": "Opus"
  },
  "workspace": {
    "current_dir": "/current/working/directory",
    "project_dir": "/original/project/directory"
  },
  "version": "1.0.80",
  "output_style": {
    "name": "default"
  },
  "cost": {
    "total_cost_usd": 0.01234,
    "total_duration_ms": 45000,
    "total_api_duration_ms": 2300,
    "total_lines_added": 156,
    "total_lines_removed": 23
  },
  "context_window": {
    "total_input_tokens": 15234,
    "total_output_tokens": 4521,
    "context_window_size": 200000,
    "used_percentage": 8,
    "remaining_percentage": 92,
    "current_usage": {
      "input_tokens": 8500,
      "output_tokens": 1200,
      "cache_creation_input_tokens": 5000,
      "cache_read_input_tokens": 2000
    }
  },
  "exceeds_200k_tokens": false,
  "vim": {
    "mode": "NORMAL"
  },
  "agent": {
    "name": "security-reviewer"
  }
}
EOF

ビルドとデプロイ

次の手順でビルド&成果物を作成し、その後所定のディレクトリに成果物をコピーして設置しています。下記のデプロイ用スクリプトを用意しました。

#!/usr/bin/env bash

set -xe

gleam build --target erlang
gleam run -m gleescript
cp ./claude_statusline $HOME/.claude/statusline

gleescriptというツールを使うとシングルバイナリを作れるようで、これを使って実行できるようにしてあります。

github.com

成果物は .claude/statusline 以下に置き、settings.jsonに起動方法を記述します。

  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline",
    "padding": 0
  }

まとめ

新しいプログラミング言語の入門に、statuslineというおもちゃを使えるという話でした。Gleamでなければならない理由はとくにないのですが、せっかくなので興味のあるプログラミング言語で遊んでみてはいかがでしょうか。AIの支援を受けながら実装するとかなり高速に入門できるので、そうした使い方をしながらぜひやってみてはいかがでしょうか。