/var/log/Sawada.log

SAINO中毒患者の備忘録。

tmuxのステータスラインをお洒落にしよう Ver.2.0.0

はじめに

こんにちは,痛風鍋が恋しくなってきたさわだです.
本記事はLOCAL学生部のAdvent Calendar 2019から拝借しています.

adventar.org

僕の前に投稿のあったmueruくんのKaitai Structというのは僕も初めて知りました.
公式サイトにあるIDEから手軽に試せるので面白かったです.

qiita.com

kodamaくんや工女さん,yamadaくんも時間のある時に投稿しておくれ!楽しみに待ってるぞい(^q^)
さて僕は普段使っているdotfiles改修の話です(何度目だdotfiles)

きっかけ

リモートサーバ複数台を使う作業が多い中Tmuxは自分にとって欠かせない存在です.一番作業で見てる画面ですから,日常的な情報の一つや二つはステータスラインに入れておきたいですよね?僕は入れておきたいです.そんな訳で以前の記事では天気を表示できるようにしました.

takuzoo3868.hatenablog.com

いつ頃だったでしょうか,Tmuxのステータスラインに利用していたYahooのお天気APIが仕様変更になり,手軽な存在では無くなりました.その辺は確かあっきぃさんがブログで言及していた気がします.アプリケーションで叩くAPIとしてはそれで良いのですが,只のシェルスクリプトで結果を取得するだけ,かつdotfilesで管理してる状況では面倒事だと感じてしまったので僕は別のAPIへ乗り換えることにしました.

また,つい最近,関東で地震が多発してることに怖くなり,地震情報の表示も欲しくなりました.本来であればtmux-powerlineを使えよ!と叫ぶ人も居るかと思います.しかし奴は既にオワコンです.2018年にメンテナンスモードになっています.代替のpowerlineは無駄にpython製で独自フォントを使うため,設定も複雑で処理は重いし拡張しにくいです.故に検索で出てくる大半のステータスラインに関する情報は(自分には)あてにならないと考えるべきです.前の記事でもtmux-powerlineについて僕は以下のように述べていますね...

いい感じの見た目とすべく,昔は先人の情報をもとにtmux-powerlineを使っていました. しかし,dotfilesで管理しにくい点,及びpyenv環境ではpowerline設定がややこしい点から廃止し,ステータスラインをフルスクラッチすることにしました.

無いものは自分で工夫して再現するしかありません.やっていきです.

Tmux weather

OpenWeatherMapのAPIへ変更することにしました.自分は予報ではなく現在の天気が知りたかったので,無料枠でそこそこ叩けるAPIとしてピッタリ*1.今までは xml 形式を正規表現で上手くパースしていたのですが,時代の流れに従い json で取得します.シェルスクリプトjson をパースする際はjqが大変便利です.sedみたいなものです.

https://stedolan.github.io/jq/

APIのドキュメントに従い,取得するお天気情報と表示内容を決めます.気温と天気がわかれば十分ですが,現時刻まで取得できるので日の出・日の入り時刻も取得して,晴れのとき夜なら月を表示する親切設計になりました.また,天気情報も英語でそのまま表示するのは残念なので,Nerd fontsに埋め込まれているWeather iconを活用します.天気用アイコンなのに宇宙人の襲来とかも想定されててなかなか面白いです.ユーモアがあります,大好き.

作成したスクリプトは以下のとおりです.

#!/usr/bin/env bash
#
# Author: takuzoo3868
# Last Modified: 27 Nov 2019.
# API: https://openweathermap.org/current
#
# NEED API KEY in .bashrc_local: WEATHER_API

ostype() { echo $OSTYPE | tr '[:upper:]' '[:lower:]'; }

export SHELL_PLATFORM='unknown'

case "$(ostype)" in
  *'linux'*    ) SHELL_PLATFORM='linux'  ;;
  *'darwin'*   ) SHELL_PLATFORM='osx'        ;;
  *'bsd'*      ) SHELL_PLATFORM='bsd'        ;;
esac

shell_is_linux() { [[ $SHELL_PLATFORM == 'linux' || $SHELL_PLATFORM == 'bsd' ]]; }
shell_is_osx()   { [[ $SHELL_PLATFORM == 'osx' ]]; }
shell_is_bsd()   { [[ $SHELL_PLATFORM == 'bsd' ]]; }

export -f shell_is_linux
export -f shell_is_osx
export -f shell_is_bsd

# Path tmp file
export DIR_TEMPORARY="/tmp/tmux-weather_${USER}"
if [ ! -d "$DIR_TEMPORARY" ]; then
  mkdir -p "$DIR_TEMPORARY"
fi

# DEFAULT
WEATHER_DATA_PROVIDER_DEFAULT="openweathermap"
WEATHER_UNIT_DEFAULT="metric" # metric:Celsius, imperial:Fahrenheit
WEATHER_UPDATE_PERIOD_DEFAULT="600"
WEATHER_LOCATION_DEFAULT="1864518" #chofu

export WEATHER_DATA_PROVIDER="${WEATHER_DATA_PROVIDER_DEFAULT}"
export WEATHER_UNIT="${WEATHER_UNIT_DEFAULT}"
case "$WEATHER_UNIT" in
    "metric")
      export WEATHER_UNIT_CASE="c"
      ;;
    "imperial")
      export WEATHER_UNIT_CASE="f"
      ;;
    *)
      export WEATHER_UNIT_CASE="k"
esac
# How often to update the weather in seconds.
export WEATHER_UPDATE_PERIOD="${WEATHER_UPDATE_PERIOD_DEFAULT}"
# Name of GNU grep binary if in PATH, or path to it.
export WEATHER_GREP="${WEATHER_GREP_DEFAULT}"
# Your location. Find a code that works for you:
export WEATHER_LOCATION="${WEATHER_LOCATION_DEFAULT}"

# Setting grep command
if shell_is_bsd  && [ -f /user/local/bin/grep  ]; then
  WEATHER_GREP_DEFAULT="/usr/local/bin/grep"
else
  WEATHER_GREP_DEFAULT="grep"
fi

__default_settings() {
  if [ -z "$WEATHER_DATA_PROVIDER" ]; then
    export WEATHER_DATA_PROVIDER="${WEATHER_DATA_PROVIDER_DEFAULT}"
  fi
  if [ -z "$WEATHER_UNIT" ]; then
    export WEATHER_UNIT="${WEATHER_UNIT_DEFAULT}"
  fi
  if [ -z "$WEATHER_UPDATE_PERIOD" ]; then
    export WEATHER_UPDATE_PERIOD="${WEATHER_UPDATE_PERIOD_DEFAULT}"
  fi
  if [ -z "$WEATHER_GREP" ]; then
    export WEATHER_GREP="${WEATHER_GREP_DEFAULT}"
  fi
  if [ -z "$WEATHER_LOCATION" ]; then
    echo "No weather location specified.";
    exit 8
  fi
}

# Run status line in tmux
__run_weather() {
  __default_settings
  local tmp_file="${DIR_TEMPORARY}/weather_openweathermap.txt"
  local weather
  case "$WEATHER_DATA_PROVIDER" in
    "openweathermap") weather=$(__openweathermap_weather) ;;
    *)
      echo "Unknown weather provider [$WEATHER_DATA_PROVIDER]";
      return 1
  esac
  if [ -n "$weather" ]; then
    echo "$weather"
  fi
}

# Get the weather from OpenWeatherMap
__openweathermap_weather() {
  if [ -f "$tmp_file" ]; then
    if shell_is_bsd; then
      last_update=$(stat -f "%m" ${tmp_file})
    elif shell_is_linux || shell_is_osx; then
      last_update=$(stat -c "%Y" ${tmp_file})
    fi
    time_now=$(date +%s)

    up_to_date=$(echo "(${time_now}-${last_update}) < ${WEATHER_UPDATE_PERIOD}" | bc)
    if [ "$up_to_date" -eq 1 ]; then
      __read_tmp_file
    fi
  fi

  degree=""
  if [ -z "$degree" ]; then
    weather_data=$(curl --max-time 4 -s "http://api.openweathermap.org/data/2.5/weather?id=${WEATHER_LOCATION}&units=${WEATHER_UNIT}&appid=${WEATHER_API}")
    # echo "$weather_data"
    if [ "$?" -eq "0" ]; then
      degree=$(echo "$weather_data" | jq .main.temp)
      condition=$(echo "$weather_data" | jq -r '.weather[] | .description')
      sunrise_unixtime=$(echo "$weather_data" | jq .sys.sunrise)
      sunset_unixtime=$(echo "$weather_data" | jq .sys.sunset)
 
      if shell_is_bsd; then
        date_arg='-j -f "%H:%M %p "'
      else
        date_arg='-d'
      fi
      sunrise=$(date ${date_arg} @${sunrise_unixtime} +%H%M)
      sunset=$(date ${date_arg} @${sunset_unixtime} +%H%M)
    elif [ -f "${tmp_file}" ]; then
      __read_tmp_file
    fi
  fi

  if [ -n "$degree" ]; then
    if [ "$WEATHER_UNIT_CASE" == "k" ]; then
      degree=$(echo "${degree} + 273.15" | bc)
    fi
    condition_symbol=$(__get_weather_image "$condition" "$sunrise" "$sunset" "$degree") 
    echo "${condition_symbol} ${degree}°$(echo "$WEATHER_UNIT_CASE" | tr '[:lower:]' '[:upper:]')" | tee "${tmp_file}"
  fi
}

# Get symbol for condition. 
# Available conditions: https://openweathermap.org/weather-conditions
__get_weather_image() {
  local condition="$1"
  local sunrise="$2"
  local sunset="$3"
  local degree="$4"

  case "$condition" in
    ## Group 800: Clear
    "clear sky")
      time_forecast=$(date +%H%M)
      if [ "$time_forecast" -ge "$sunset" -o "$time_forecast" -le "$sunrise" ]; then
        if [ "$degree" -le 5 ]; then
          echo ""
        else
          echo ""
        fi
      else
        if [ "$degree" -ge 25 ]; then
          echo "" # weather_hot
        else
          echo ""
        fi
      fi
      ;;

    ## Group 80x: Clouds
    "few clouds")
      echo ""
      ;;
    "scattered clouds")
      echo ""
      ;;
    "broken clouds" | "overcast clouds")
      echo "" # fa_cloud
      ;;

    ## Group 7xx: Atmosphere
    "dust" | "Haze" | "Smoke" | "sand" | "dust whirls")
      echo "" # weather_dust
      ;;
    "fog" | "mist")
      echo "" # weather_fog
      ;;
    "volcanic ash")
      echo ""
      ;;
    "tornado" | "tropical storm" | "hurricane")
      echo "" # weather_hurricane
      ;;

    ## Group 5xx: Rain & Group 3xx: Drizzle
    "rain" | "light rain"  | "moderate rain" | "heavy intensity rain" | "drizzle" | "light intensity drizzle" | "heavy intensity drizzle" | "light intensity drizzle rain" | "drizzle rain")
      echo ""
      ;;
    "shower rain" | "scattered showers" | "very heavy rain" | "extreme rain" | "light intensity shower rain" | "heavy intensity shower rain" | "ragged shower rain" | "heavy intensity drizzle rain" | "shower rain and drizzle" | "heavy shower rain and drizzle" | "shower drizzle" | "squalls")
      echo "" # weather_showers
      ;;
    "mixed rain and snow" | "mixed rain and sleet" | "freezing drizzle" | "freezing rain" | "mixed rain and hail" | "Light rain and snow" | "Rain and snow")
      echo "" # weather_rain_mix
      ;;
    
    ## Group 6xx: Snow
    "Snow" | "light snow" | "Heavy snow" | "Sleet" | "Light shower sleet" | "Shower sleet" | "Light shower snow" | "Shower snow" | "Heavy shower snow")
      echo ""
      ;;
    
    ## Group 2xx: Thunderstorm
    "thunderstorm with light rain" | "thunderstorm with rain" | "thunderstorm with heavy rain" | "light thunderstorm" | "thunderstorm" | "heavy thunderstorm" | "ragged thunderstorm" | "thunderstorm with light drizzle" | "thunderstorm with drizzle" | "thunderstorm with heavy drizzle")
      echo "" # weather_lightning
      ;;
    
    "not available")
      echo "" 
      ;;
    *)
      echo "" # unknown
      ;;
  esac
}

__read_tmp_file() {
  if [ ! -f "$tmp_file" ]; then
    return
  fi
  cat "${tmp_file}"
  exit
}

# exec
__run_weather

データの取得は weather_data=$(curl --max-time 4 -s "http://api.openweathermap.org/data/2.5/weather?id=${WEATHER_LOCATION}&units=${WEATHER_UNIT}&appid=${WEATHER_API}")です.curl使ってjson形式のお天気データを取ってきてます.WEATHER_LOCATION は天気を知りたい地点, WEATHER_UNIT は摂氏・華氏・ケルビンの選択になります.難点としてはAPIキーが必須になったことです.この点は bashrc_local へ記載したキー値を読み込む形式で対応するしかなさそうです.もっとスマートな管理方法があったら教えて下さい.

f:id:takuzoo3868:20191214015016p:plain

その他にも負荷軽減のために,取得データの一時ファイルを作成し,作成時のタイムスタンプと現在時刻との差が設定時間を超えない限り,一時ファイルの内容を表示してデータ更新は行わないよう工夫してあります.一部過去のpowerlineに入っていたスクリプトを参考にしました.ソースコード中の四角で文字化けになってる箇所は,実際はこの様にフォントがハードコーディングされています.

f:id:takuzoo3868:20191214021546p:plain

Tmux earthquake

国産のAPIであるP2P地震情報 JSON APIを利用します.本来は利用者の揺れたという指標を元に各地での地震の影響範囲を推定することが目的のサービスなのですが,なんと嬉しいことに気象庁地震情報と津波情報をJSONで取得できるのです,すごい有り難い.

www.p2pquake.net

何を表示すべきか非常に迷いましたが,作業中はとりあえず「地震があったこと」と「どこでどの程度の震度・津波の有無」がわかればサッとTwitterを見に行けると思いました(おい).なので最終的な表示はこんな感じです.天気もそうですが文字を極力使わないのは,文字で溢れているCUIの中でも視認性を高めるためです.あとは絵文字が好きだから

スクリプト内の変数として,プロバイダ,データ更新間隔,過去何時間までの地震を表示するか,地震の取得件数(最新のみなので1),震度幾つ以上のデータを取るか設定します.例としては以下の通り.

EARTHQUAKE_DATA_PROVIDER_DEFAULT="p2pquake"
UPDATE_PERIOD_DEFAULT="200"
ALERT_TIME_WINDOW_DEFAULT="200" # min
EARTHQUAKE_GET_DATA_LIMIT_DEFAULT='1'
EARTHQUAKE_MIN_SCALE_DEFAULT='10'

export EARTHQUAKE_DATA_PROVIDER="${EARTHQUAKE_DATA_PROVIDER_DEFAULT}"
export UPDATE_PERIOD="${UPDATE_PERIOD_DEFAULT}"
export ALERT_TIME_WINDOW="${ALERT_TIME_WINDOW_DEFAULT}"
export EARTHQUAKE_GET_DATA_LIMIT="${EARTHQUAKE_GET_DATA_LIMIT_DEFAULT}"
export EARTHQUAKE_MIN_SCALE="${EARTHQUAKE_MIN_SCALE_DEFAULT}"

当然この辺も bashrc_local などへ記載してベストな値を探ってみても良い気がします.今後の課題ですね.データの取得・整形・表示関連は以下のスクリプトを作りました.

# Run status line in tmux
__run_earthquake() {
  __default_settings
  local tmp_file="${EARTHQUAKE_DIR_TEMPORARY}/earthquake.txt"
  local earthquake
  case "$EARTHQUAKE_DATA_PROVIDER" in
    "p2pquake") earthquake=$(__p2pquake_earthquake) ;;
    *)
      echo "Unknown earthquake-information provider [${EARTHQUAKE_DATA_PROVIDER}]";
      return 1
  esac
  if [ -n "$earthquake" ]; then
    echo "${earthquake}"
  fi
}

# Get earthquake information from p2pquake
__p2pquake_earthquake() {
  if [[ -f "$tmp_file" ]]; then
    if shell_is_bsd; then
      last_update=$(stat -f "%m" ${tmp_file})
    elif shell_is_linux || shell_is_osx; then
      last_update=$(stat -c "%Y" ${tmp_file})
    fi
    time_now=$(date +%s)

    up_to_date=$(echo "(${time_now} - ${last_update}) < ${UPDATE_PERIOD}" | bc)
    if [ "$up_to_date" -eq 1 ]; then
      __read_tmp_file
    fi
  fi

  magnitude=""
  if [ -z "$magnitude" ]; then
    # get the rss file
    data=$(curl --max-time 4 -s "https://api.p2pquake.net/v2/jma/quake?limit=${EARTHQUAKE_GET_DATA_LIMIT}&min_scale=${EARTHQUAKE_MIN_SCALE}")
    
    if [ "$?" -eq "0" ]; then
      location=$(echo $data | jq -r .[].earthquake.hypocenter.name)
      magnitude=$(echo $data | jq .[].earthquake.hypocenter.magnitude)
      scale=$(echo $data | jq .[].earthquake.maxScale)
      tsunami=$(echo $data | jq -r .[].earthquake.domesticTsunami)
      timestamp=$(echo $data | jq -r .[].earthquake.time)

      if shell_is_bsd; then
        date_arg='-j -f "%H:%M %p "'
      else
        date_arg='-d'
      fi
      timestamp_24=$(date ${date_arg} "${timestamp}" +%H:%M)

    elif [ -f "$tmp_file" ]; then
      __read_tmp_file
    fi
  fi

  if [ -n "$magnitude" ]; then
    if __check_alert_time_window ; then
      scale_jp=$(__get_earthquake_scale "$scale")
      tsunami_jp=$(__get_tsunami_info "$tsunami")
      echo "${location} ${timestamp_24}${scale_jp}${tsunami_jp} "  | tee "${tmp_file}"
    fi
  fi
}
__check_alert_time_window() {
  unixtime=$(date -d "${timestamp}" +%s)

  # (now - occurred_time)/60 < ALERT_TIME_WINDOW ?
  [[ $(( ( $(date +%s) - unixtime ) / 60 )) -lt $ALERT_TIME_WINDOW ]]
}

__get_earthquake_scale() {
  local scale="$1"

  # https://www.p2pquake.net/develop/api-v2/
  case "$scale" in
    "10")
      echo "1"
      ;;
    "20")
      echo "2"
      ;;
    "30")
      echo "3"
      ;;
    "40")
      echo "4"
      ;;
    "45")
      echo "5弱"
      ;;
    "50")
      echo "5強"
      ;;
    "55")
      echo "6弱"
      ;;
    "60")
      echo "6強"
      ;;
    "70")
      echo "7"
      ;;
    *)
      echo "" # unknown
      ;;
  esac
}

__get_tsunami_info() {
  local tsunami="$1"

  case "$tsunami" in
    "Warning")
      echo "津波予報"
      ;;
    "Watch")
      echo "津波注意報"
      ;;
    "NonEffective")
      echo "影響なし"
      ;;
    "Checking")
      echo "調査中"
      ;;
    "Unknown")
      echo "不明"
      ;;
    "None")
      echo "なし"
      ;;
  esac
}

基本の仕組みはお天気のスクリプトと変わりません.APIを叩いてJSONをパースして,一時ファイルに表示用データを保存の流れになります.少し違うのは,天気が常に表示するものに対して,地震情報は発生したときのみ表示する点です.この点は ALERT_TIME_WINDOW で設定します.また,震度の概念は日本特有なので,海外仕様に合わせるならマグニチュードの記載でも良いかもしれないです. 津波情報については予報が出ているかそうでないかの表示しか現在は対応していませんが,tsunami APIを活用して表示する情報を増やしてもいいかなと思っています. 今回,自分はTmux内に地震情報を取得できるスクリプトを導入しましたが,世の中にはVimに表示する方もいらっしゃるようです.同志の匂いがしました.

github.com

課題

とりあえず,アドカレに間に合うよう実装は終わらせましたが,当然課題はまだまだあります.
お天気なら

  • 地点などの設定変更用オプションの追加

  • デバッグ用にverbose modeの追加

地震アラートなら

  • 最適な表示方法

  • 津波情報をより詳細に展開するための条件分岐

  • proxy環境対応

などなどです.自分のdotfilesはコミット数が192もあるのですが,まだ暫くは増える気がします()

おわりに

今回実装したスクリプト.tmux_local/confこちらのように組み込む事で,最終的な今のステータスラインは以下のようになっています.

f:id:takuzoo3868:20191214025603p:plain

いい感じですね.イケイケです!快適作業環境は復活しました.これにてめでたしめでたし!

また,ここがクソなんだけど!のような感想があればdotfilesのリポジトリへissue投げて欲しいです.優先的に改修します*2

github.com

次は工大の後輩であるnayutaくんがなにか書くようです.楽しみに次の日を待ちたいと思います. それではまた別のアドカレで!

*1:無料枠で1 call/sという破格の優しさ

*2:プルリクでもいいのよ