Iori's Blog

ターミナルで雨雲レーダーを見れるOSSの開発で初めてスターを獲得した話

Published on: Sat Jun 13 2026 00:00:00 GMT+0000 (Coordinated Universal Time)

はじめに

最近はclaudecodeでかなり1つのプログラムを完成させるのが早くなっているので、何かしらのOSSを作ったり、WEBアプリを作ってGitHubに公開する、といったことにハマっていました。

ただ悲しいことにWEBアプリを作ったりしても、割とニッチなものを作っていたので全く閲覧されないし、作ったOSSに何も反応がないというのは少し悲しいものがありました。 そこで次は自分が作りたいものの中で、何かしらGitHubでスターが貰えるものを作ろう!と意気込み、termrainというものを開発しました。

iorinu/termrain
0

今の所スターが12個も貰えました!ガチで嬉しい!

分析

まずスターを獲得するにあたって、どのようなものがスターを取りやすいのかを分析しました。

基本的にGitHubというのはXのタイムラインのように知らない人のリポジトリが流れてきたり、といったことはありません。 そこで重要になってくるのが宣伝を行う、ということです。

色々調べたところ、redditやHackerNewsで宣伝を行うのが効果的であるとわかりました。

そこでまずは今まで作ってあったpwatchについて宣伝を行ってみました。

iorinu/pwatch
0

HackerNewsとredditのr/rustで宣伝を行おうと考えましたが、どちらもカルマが足らず、そもそも投稿できません。 また、r/rustはAIにまあまあ厳しそうで、このプログラムの宣伝は少し難しいと考えました。

そこで比較的そのあたりのルールがゆるい、r/tuiで宣伝を行いました。

結果としてはredditのいいねのようなものが1つ、スターは勿論つきません。

そこでredditに張り付き、比較的ウケやすい傾向を探しました。

これはr/tuiを見ていたのもありますが、以下の傾向がありました。

あたりが重要に感じました。

特にスクショがかっこいいというのがかなり重要で、掲示板を見ていて、まずタイトルを読む→スクショを見る→本文を見るといった流れはほとんどの人が通ると思いますが、 この本文までいくかどうかはスクショで決まると思います。

自分の経験で面白いな〜と思ったのはスクショがかっこいいものです。意外と人間は文章を読みません。

GitHubユーザーが好きそうなもの、というのは当たり前ではあって、スターをつけるのはGitHubユーザーなのでここにウケないと意味がありません。

そこで今回作るものの前提として

の前提から何を作るか考えました。

作ったもの

前提のredditで宣伝できるもの、見た目がかっこいい、の2つを満たすものとして何かしらのTUIを作るのがやりやすいと考えました。 ただ何か新しいサービスだったりを実装するのは少し難しいと考えたので、既存のサービスをターミナルに持ち込む方向で考えました。

そこで自分が不便に感じているもので、まだ存在しないものを実装することにしました。

例えばdiscordなんかがターミナルで見れれば便利だと思いましたが、すでに存在していました。

chojs23/concord
0

これ結構便利で自分も使わせてもらってます。

自分が思いつくものだともう結構あるな〜とは思っていたのですが1つだけありました。 雨雲レーダーを描画するものはまだありませんでした。 天気を表示するものはいくつかあるのですが、雨雲レーダーまでは対応しておらず、自分は天気を見る際は雨雲レーダーまでセットで見ているのでこれは自分も欲しいし、 割と刺さるのではと考え、決定しました。 スマホ出さずにレーダー見れたら結構嬉しいです。 termrainの画面

求める仕様

以下のものを仕様として想定しました

特にGitHubでスターという観点では世界中で使える、簡単なインストールあたりは重要であると考えました。

またこの中で雨雲レーダーの未来を見る機能は日本限定となりました。 データを持ってきているところが無料版だと未来は対応していなかったためです。

仕様技術

レイヤ採用したもの採用理由
言語Rust自分が一番慣れてる、TUIが作りやすい
TUIratatuiRustのTUIは多分これがスタンダード
画像表示ratatui-imageKitty graphics protocol / Sixel / iTerm2 を一括で抽象化してくれる。フォールバックも勝手にやってくれる
HTTPreqwest + tokio + futures非同期で並列に情報取得
CLIclap一番使ってる。clap_complete でzsh/bash/fish補完も生成
設定directories + toml + serdeXDG準拠の ~/.config/termrain/config.toml に保存。directories でOS別パスを吸収
天気情報気象庁ナウキャスト + Open-Meteo日本用に詳しめの情報 + 全世界対応データ
レーダー情報気象庁ナウキャスト + RainViewer上と同じ
配布GitHub Actions + Homebrew tapタグpushでクロスコンパイル → リリース → tap自動更新

これは最終的にこうなった感じでレーダー情報なんかは最初っからは決められてないです。

そもそも技術選定の部分は自分で全部決めたわけではなくて、AI (claudecode) に相談しながら絞り込みました。 Rust とか clap、ratatui くらいまでは普段から使ってるので自分で選んだんですが、画像表示の ratatui-image や設定回りの directories クレートあたりは「ターミナルで画像出すならどのライブラリ使うのがいい?」みたいに聞いて、提示されたものを見つつ採用した感じです。

完全に自力で技術選定できる人になりたい気持ちはありつつ、AI と相談しながら「この場面ならこれ」って当て込んでいけると進みが圧倒的に速いので、最近は割とこのスタイルで作ってます。 これを選べるようになるのは経験なのでしょうか?

CI/CDあたりも同じ流れでほぼAIにやってもらったのであまり分かっていませんが、GitHub Actionsかなり便利ですね。 どっかでちゃんと勉強してみたいです。

データの取得

このプログラムでは指定地点の

を外部から取得しています。 特に並列化が必要であった地図データと降水量データについてどのように取得したかをこのセクションでは書きます。

何を並列にしたいのか

レーダーを 1 フレーム表示するために取りに行くデータは以下です。

これを順番に取りに行くと結構遅いです。

ということで全部並列で投げて、一番遅いリクエストの時間で揃うように作りました。

Rust の async 並列パターン

「複数の HTTP リクエストを並列に送る際の方法」を AI に聞いたら、主に 3 つのパターンがあるらしいことがわかりました。

// ① 同種の future を Vec で並列実行
let results = futures::future::join_all(map_fetches).await;

// ② 異種の future を tuple で並列実行
let (rain, map_results) = tokio::join!(rain_future, futures::future::join_all(map_fetches));

// ③ tokio::spawn で個別タスクとしてランタイムに投げる
let h1 = tokio::spawn(fetch_a());
let h2 = tokio::spawn(fetch_b());
let (r1, r2) = tokio::try_join!(h1, h2)?;

それぞれメリットが違っていて、

という感じです。

①と②は「タスクとしてランタイムに投げない、ただ複数 future を同時にポーリングするだけ」なので、ワーカースレッドは増えませんが async ランタイム上ではちゃんと並列に進みます。HTTP みたいな IO 待ち中心の用途ならこれで十分ということでした。

逆に③の tokio::spawn は本当に別タスクとして走る代わりに 'static ライフタイムが必要で、self を借用したまま投げると怒られます。Arc<Self> で包んで move する手はありますが、コードが急に重たくなるそうです。

termrain は

という条件なので、①と②を組み合わせる 「同種は join_all でまとめて、異種は tokio::join! で束ねる」 のが一番ハマる、という結論になりました。

一時期非同期プログラミングを勉強したくてtokioを勉強していたことがあったのですが、途中で挫折してしまい、理解がしきれていませんでした。 これを機にまたtokio及び非同期プログラミング勉強してみようと思います。 特に外部APIを使うようなものだと必須だと感じました。

ここまで並列の話をしているのですが実は最初の実装はかなり素朴な直列でした。 降水量を POST で取り終わってから、おもむろに地図タイルを並列フェッチするという流れです。

// 旧コード: 雨雲 POST が終わるまで地図タイル取得が始まらない
let arr: Vec<MultiCurrent> = self
    .client
    .post("https://api.open-meteo.com/v1/forecast")
    .json(&body)
    .send()
    .await?               // ← ここで POST 完了を待つ
    .error_for_status()?
    .json()
    .await?;              // ← レスポンス JSON の読み込みも待つ

// ここから初めて地図タイル fetch を組み立て始める
let mut map_fetches = Vec::with_capacity(15);
for dy in -1..=1 {
    for dx in -2..=2 {
        // ...
        map_fetches.push(async move { ... });
    }
}
let map_results = futures::future::join_all(map_fetches).await;

map_fetches の内側はちゃんと並列なんですが、雨雲 POST と地図タイル取得は互いに何の依存もないのに直列に並んでいます。

// 修正版: 雨雲 POST と地図タイル取得を同時に進める
let rain_future = self.client.post(API_BASE).json(&body).send();
let (rain_resp, map_results) = tokio::join!(
    rain_future,
    futures::future::join_all(map_fetches),
);
let arr: Vec<MultiCurrent> = rain_resp?.error_for_status()?.json().await?;

雨雲データのソース選びで詰まった

ここまで「雨雲は RainViewer のタイルを使う」前提で書いてきたんですが、最初からこの構成だったわけではなくて、ソースを乗り換えています。

詰まったポイントが Rust うんぬんよりは「外部 API どう使うか」の話なんですが、わりと書く価値はあるかなと思ったので残しておきます。

最初は Open-Meteo の多地点クエリ

天気予報を Open-Meteo に投げているので、雨雲も Open-Meteo で取れたら一番ラクだと考えました。 Open-Meteo には「複数地点の現在の降水量を一度に問い合わせる」エンドポイントがあるので、表示範囲内に 32x16 = 512 点のグリッドを敷いて、各点の降水量をまとめて取りに行く方法を取りました。

// 旧コード: 表示範囲に grid を敷いて lat/lon を全部クエリにぶら下げる
let width = 32;
let height = 16;
let mut lats = vec![];
let mut lons = vec![];
for j in 0..height {
    for i in 0..width {
        // view 範囲を等分して各セルの中心座標を出す
        lats.push(format!("{lat_j:.4}"));
        lons.push(format!("{lon_i:.4}"));
    }
}
let url = format!(
    "https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}\
     &current=precipitation&timezone=auto",
    lats.join(","),
    lons.join(","),
);
let arr: Vec<MultiCurrent> = self.client.get(&url).send().await?.json().await?;

これで返ってきた 512 点の降水量を 2 次元グリッドに並べ直して、bilinear 補間で出力ピクセル単位の降水量にしてカラーマッピングする、という方針でした。

ローカルテストでは動いていたんですが、コルカタなど雨が強い都市で確認しようとしたところレーダーが永遠に読み込み中になりました。

HTTP 414 Request-URI Too Large

ログを見ると nginx から HTTP 414 が返っていました。URL が長すぎるという意味のエラーコードらしいです。

URL の長さを計算してみたら 8285 文字 ありました。Open-Meteo の前段の nginx には URL 長の上限があって、その手前で弾かれていたわけです。

URL length: 8285
Points: 512

GET ではなく POST にすれば URL 長制限は回避できるので、JSON ボディに lat/lon の配列を入れる形で POST に切り替えました。

let body = serde_json::json!({
    "latitude": lats,
    "longitude": lons,
    "current": ["precipitation"],
    "timezone": vec!["auto"; lats.len()],
});
let arr: Vec<MultiCurrent> = self
    .client
    .post("https://api.open-meteo.com/v1/forecast")
    .json(&body)
    .send()
    .await?
    .error_for_status()?
    .json()
    .await?;

これで 414 は解消し、レーダーが描画できるようになりました。

HTTP 429 Too Many Requests

しばらく触っていると、今度は HTTP 429 が出るようになりました。

レート制限らしいです。 少し触っただけで枯渇する仕様としては厳しいなと思って Open-Meteo のドキュメントを確認すると、衝撃の事実がわかりました。

多地点クエリは、地点の数だけ API コール扱い になります。512 点 = 512 コール。

p でアニメーション再生していたら 1 秒あたり 1 フレーム取りに行くので、1 フレーム = 512 コール × 数フレームで、フリー枠の 1 時間 5000 コールがあっという間に消し飛びます。POST にしたところで、消費するクオータ自体はそのままです。

ここで「Open-Meteo を多地点で叩いて雨雲画像を組み立てる」アプローチに無理があると判断し、雨雲データの取り方を見直すことにしました。

RainViewer に乗り換え

色々調べた結果、RainViewer という選択肢にたどり着きました。

特に「タイル画像で返してくれる」というのが大きくて、Open-Meteo 多地点アプローチが「数値を 512 点取って自前で色付け」だったのに対して、RainViewer なら「事前に色付けされた PNG をそのまま地図に重ねるだけ」になります。

API はインデックス JSON を 1 回叩いて、そこで返ってきた最新フレームの path を使ってタイル PNG を取りに行く、という 2 ステージ構成です。

// 1. インデックスを取って、利用可能なフレーム一覧を取得
let index: RvIndex = self
    .client
    .get("https://api.rainviewer.com/public/weather-maps.json")
    .send()
    .await?
    .json()
    .await?;

// 2. 最新フレームの URL からタイル画像を取得
let url = format!("{}{}/256/{z}/{tx}/{ty}/2/1_1.png", index.host, frame.path);

これで 1 フレーム表示するのに必要なリクエスト数が、多地点クエリ 1 個(512 コール扱い)から、インデックス 1 + タイル数枚に減りました。

最初から RainViewer 構成にできていれば一番ラクではあるんですが、 「天気予報も雨雲もまとめて Open-Meteo でいけそう」という見立てから入ってしまうとここまで全部の段差を踏んでしまうので、 初手で雨雲のデータソースを別に分けて考えるべきだったというのが教訓です。

宣伝

なんだかんだあって完成はしたのでこっからが重要な宣伝フェーズに入ります。 幸運なことに作り終わったぐらいでClaude Fable5が使えるようになり、これを用いてデザインを改善したところ劇的に改善しました。 正直デザインは完全にAI頼りなところがあるのでかなりデカいです。

予定通りredditのr/tuiに投稿、一晩置いてみると… redditの画面 なんと20ぐらいいいねがついていました(これは記事作成時点のやつ)。 大体100人に1人ぐらいがいいねしてくれてました。 GitHubに確認しにいくとスターも7個ぐらい貰えていました!

終わりに

今回はスターを獲得するというOSSに貢献というよりは下世話な目的からスターを狙ってみたのですが、やはり誰かにいいと思ってもらえるのは嬉しいですね。 まあ自分にとっても欲しいものだったのでいいのですが。 この誰かに価値を届ける視点は今後の開発でも持っておきたいと感じました。

Tags:

programming

Rust

TUI

OSS