/var/log/Sawada.log

SAINO中毒患者の備忘録。

CUIでお洒落な開発環境を整えよう

はじめに

みなさん年の瀬いかがお過ごしでしょうか.こたつスウィングバイが止まらないさわだです.
工大アドベントカレンダー(一日目)の空枠を拝借して,自分も最近の事を書こうと思います.

adventar.org

今回の記事はこれです.

きっかけ

我が愛機であるThinkpad X220にArch LinuxをホストOSとしてインストールしましたが,chrome君とslack君に加え,Intellijの子供たちを使うとメモリが逼迫するのでCUIで開発環境を整えたいなと思ったのがきっかけです.

整備する

以前はneovim周辺の設定を弄って満足出来ましたが,あらゆる事をCUIで完結させたい欲が高まるにつれ,複数のターミナルを起動するのも億劫になってきました. 更にCUIでも毎日触るのが楽しくなるような見た目が欲しくなりました. となると...自分にとって必要な環境整備は以下の項目が挙げられます.

  • CUIツールの導入

  • ターミナルマルチプレクサの導入(且つ,ばぁぁーーんといい感じに起動したい)

  • 諸々の見た目の整備

では順を追って解説に入ります.

CUIツール

必要なものはすべて偉大なるArch Wikiが教えてくれました.アプリケーション一覧にあるコンソールに様々な紹介があります*1

システム監視として見た目がいい感じかつGo製のgotop,ファイルマネージャとして操作感がvimに似たranger,作業BGMを聴くためにプレイリストが管理できるmps-youtubeIRCクライアントとしてweechatを導入しました.

Archではyay -S <pkg>でこれら全てが楽にインストール出来ます.他OSもドキュメントに従えば楽にツールを導入できます.

gotop

vimに似た操作感でプロセス管理も可能です.詳しくはリポジトリのREADMEを読めばいいと思います.nodejs製のgtopはモニタリングツールとして有名ですが,CUIで使うにはちょっと軽量とは言い難い部分があります.私の環境ではgotopによりメモリ使用量はgtop 85MB から gotop 7.3MBへ節約できました.gotop -c solarizedでカラースキーム指定起動するとこんな画面です.tmux上ではminimal表示を利用しています.

f:id:takuzoo3868:20181228003551p:plain

ranger

vimに似た操作感でファイル操作ができます.設定ファイルは.config/ranger配下に作られるので,dotfilesによる設定管理も容易です.自分は見た目のカスタマイズにranger_deviconsを使っています.カラースキームはsnowを採用.

github.com

f:id:takuzoo3868:20181228004601p:plain

mps-youtube

作業用にyoutubeにあるnightcoreを聴きたいけれど,ブラウザは使いたくない自分がたどり着いたのはこのツールでした.Python製です.操作キーは上記2つのツールに比べると若干特殊なため覚える必要があります.起動はmpsyt.操作方法は以下の記事を見ればよいかと.自分はキーワード検索/hogefugaして全曲再生all,またはプレイリスト再生pl <playlist ID>を使うことが多いです.

www.blky.me

f:id:takuzoo3868:20181228010252p:plainf:id:takuzoo3868:20181228010305p:plain

weechat

IRCチャット用に軽量なweechatは様々なプラグインで拡張が出来ます.ただし,プラグインは言語依存があるので注意が必要です.入りたいIRCサーバを追加したり,バッファにチャンネルリストを表示したり,色々設定すればGUIに負けないチャットツールとして重宝できるでしょう.今は開発者コミュニティで情報収集やPKhack界隈のチャットとして使っていますが,できればtwitterやslackもweechatで表示したいと格闘中...

f:id:takuzoo3868:20181228011416p:plain

ターミナルマルチプレクサ

日本語の記事も多いtmuxを導入しました.sshを多用する場面でも何かと重宝するので以前から入っていましたが,本格的に設定したのは今回が初めてです. tmuxをいい感じに起動するためにセッションマネージャとしてtmuxpを導入します.

github.com

これは,競合であるtmuxinatorがRuby依存だったのに対して,tmuxpがpython依存だったからです(すでにpyenv+pipenvで環境構築していたため).この辺は好きなセッションマネージャを導入すればいいと思います.設定にはyamlを使います.

tmux_options: -2
session_name: work
root: ~/CTF/

windows:
- window_name: main
  layout: 86f2,151x37,0,0{84x37,0,0[84x22,0,0,2,84x14,0,23,6],66x37,85,0[66x10,85,0,3,66x26,85,11,4]}
  panes:
    - screenfetch
    - gotop -c solarized -m
    - ranger
    - mpsyt pl PLBFICLTzW2LckbdM1Ri6BBfpeSPwRYzXe
- window_name: chat
  panes:
    - weechat
- window_name: work
  focus: true
  layout: cab4,151x37,0,0{92x37,0,0,6,58x37,93,0,7}
  panes:
    - pane
    - pane

layoutの項目はtmux起動中にtmux list-windowsで調べることが出来ます.上記のような設定を書いて.tmux配下などに保存しておきtmuxp load path/to/config.yamlで起動するとウィンドウが複数立ち上がり,分割されたペイン上でそれぞれのCUIツールを走らせることが出来ます.大変便利.

諸々の見た目

Nerd Fonts

ターミナル上にもアイコン系ロゴ系フォントを導入します.理由はascii artのような見た目の楽しさと,neovimで既にairline用としてpowerline系のフォントを使っていたからです. 有名所をまとめた素晴らしいロゴフォントとしてNerd Fontsがあります.

github.com

このフォントを普段使うターミナル用フォントにfontforgeで合成し設定することで,ターミナルの表示はガラリと変化するはず.手っ取り早く導入したい方は日本語プログラミング環境用等幅フォントCica fontをおすすめします.ライセンス関係で一部表示できないため,どうしてもアイコン類を表示したい方はfontforgeを使って上書きすれば良いかと.日本語表示には影響がないので...

ターミナルでの表示確認用にシェルスクリプトを書きました.もともとNerd Fontsに同封されていた使いづらいテストコードをコマンドツールとして改良したものになります.

#!/usr/bin/env bash

# Author: takuzoo3868
# Last Modified: 07 Dec 2018.

# Check Nerd fonts drawing on terminal.
# Nerd Fonts Version: 2.0.0
# Script Version: 1.0.0

PROGNAME=$(basename $0)
VERSION="1.0.0"


usage() {
  echo "usage: ${PROGNAME} [--font_option]"
  echo ""
  echo "Check Nerd fonts v.2.0.0 drawing on terminal."
  echo ""
  echo "optional arguments:"
  echo "  -h, --help            show this help message and exit"
  echo "  --dev                 Font Devicon"
  echo "  --fa                  Font Awesome"
  echo "  --fae                 Font Awesome Extension"
  echo "  --iec                 IEC Power Symbols"
  echo "  --linux, --fontlogos  Font Linux and other open source Glyphs"
  echo "  --material, --mdi     Material Design Icon"
  echo "  --oct                 Octicons"
  echo "  --pl                  Powerline"
  echo "  --ple                 Powerline Extra"
  echo "  --pom                 Pomicon"
  echo "  --seti                Seti-UI + Custom"
  echo "  --weather             Weather Icons"
  exit 1
}

# Given an array of decimal numbers print all unicode codepoint.
function print-decimal-unicode-range() {
  local originalSequence=("$@")
  local counter=0
  # Use alternating colors to see which symbols extend out of the bounding
  # box.
  local bgColorBorder='\033[48;5;8m'
  local bgColorCode='\033[48;5;246m'
  local alternateBgColorCode='\033[48;5;240m'
  local bgColorChar='\033[48;5;66m'
  local alternateBgColorChar='\033[48;5;60m'
  local underline='\033[4m'
  local currentColorCode="${bgColorCode}"
  local currentColorChar="${bgColorChar}"
  local reset_color='\033[0m'
  local allChars=""
  local allCodes=""
  local wrapAt=5
  local topLine="${bgColorBorder}╔══════╦══════╦══════╦══════╦══════╗${reset_color}"
  local bottomLine="${bgColorBorder}╚══════╩══════╩══════╩══════╩══════╝${reset_color}"
  local line="${bgColorBorder}╠══════╬══════╬══════╬══════╬══════╣${reset_color}"
  local bar="${bgColorBorder}${reset_color}"
  local originalSequenceLength=${#originalSequence[@]}
  local leftoverSpaces=$((wrapAt - (originalSequenceLength % wrapAt)))

  # add fillers to array to maintain table:
  if [[ "$leftoverSpaces" < "$wrapAt" ]]; then
    # shellcheck disable=SC2034
    # needs rework without 'i' var?
    for i in $(seq 1 $leftoverSpaces); do
      originalSequence+=(0)
    done
  fi

  local sequenceLength=${#originalSequence[@]}

  printf "%b\\n" "$topLine"

  for decimalCode in "${originalSequence[@]}"; do
    local hexCode
    hexCode=$(printf '%x' "${decimalCode}")
    local code="${hexCode}"
    local char="\\u${hexCode}"

    # fill in placeholder cells properly formatted:
    if [ "${char}" = "\\u0" ]; then
      char=" "
      code="    "
    fi

    allCodes+="${currentColorCode} ${underline}${code}${reset_color}${currentColorCode} ${reset_color}$bar"
    allChars+="${currentColorChar}  ${char}   ${reset_color}$bar"
    counter=$((counter + 1))
    count=$(( (count + 1) % wrapAt))

    if [[ $count -eq 0 ]]; then

      if [[ "${currentColorCode}" = "${alternateBgColorCode}" ]]; then
        currentColorCode="${bgColorCode}"
        currentColorChar="${bgColorChar}"
      else
        currentColorCode="${alternateBgColorCode}"
        currentColorChar="${alternateBgColorChar}"
      fi

      printf "%b%b%b" "$bar" "$allCodes" "$reset_color"
      printf "\\n"
      printf "%b%b%b" "$bar" "$allChars" "$reset_color"
      printf "\\n"

      if [ "$counter" != "$sequenceLength" ]; then
        printf "%b\\n" "$line"
      fi

      allCodes=""
      allChars=""
    fi

  done

  printf "%b\\n" "$bottomLine"

}

function print-unicode-ranges() {
  echo ''

  local arr=($@)
  local len=$#
  local combinedRanges=()

  for ((j=0; j<len; j+=2)); do
    local start="${arr[$j]}"
    local end="${arr[(($j+1))]}"
    local startDecimal=$((16#$start))
    local endDecimal=$((16#$end))

    combinedRanges+=($(seq "${startDecimal}" "${endDecimal}"))

  done

  print-decimal-unicode-range "${combinedRanges[@]}"

}


for OPT in "$@"
do
  case $OPT in
    '-h' | '--help' )
      usage
      ;;

    '--dev' )
      echo "Nerd Fonts - Devicons"
      print-unicode-ranges e700 e7c5
      echo; echo
      ;;

    '--fa' )
      echo "Nerd Fonts - Font awesome" 
      print-unicode-ranges f000 f2e0
      echo; echo
      ;;

    '--fae' )
      echo "Nerd Fonts - Font awesome extension" 
      print-unicode-ranges e200 e2a9
      echo; echo
      ;;

    '--ice' )
      echo "Nerd Fonts - Font Power Symbols"
      print-unicode-ranges 23fb 23fe 2b58 2b58
      echo; echo
      ;;

    '--linux' | '--fontlogos')
      echo "Nerd Fonts - Font Linux"
      print-unicode-ranges f300 f31c
      echo; echo
      ;;

    '--material' | '--mdi')
      echo "Nerd Fonts - Material Design Icons"
      print-unicode-ranges f500 fd46
      echo; echo
      ;;

    '--oct' )
      echo "Nerd Fonts - Octicons"
      print-unicode-ranges 2665 2665 26A1 26A1 f400 f4a8 f67c f67c
      echo; echo
      ;;

    '--pl' )
      echo "Nerd Fonts - Powerline"
      print-unicode-ranges e0a0 e0a2 e0b0 e0b3
      echo; echo
      ;;

    '--ple' )
      echo "Nerd Fonts - Powerline Extra"
      print-unicode-ranges e0a3 e0a3 e0b4 e0c8 e0cc e0d2 e0d4 e0d4
      echo; echo
      ;;

    '--pom' )
      echo "Nerd Fonts - Pomicons"
      print-unicode-ranges e000 e00a
      echo; echo
      ;;

    '--seti' )
      echo "Nerd Fonts - Symbols original"
      print-unicode-ranges e5fa e62e
      echo; echo
      ;;

    '--weather' )
      echo "Nerd Fonts - Weather Icons"
      print-unicode-ranges e300 e3eb
      echo; echo
      ;;

    -*)
      echo "$PROGNAME: illegal option -- '$(echo $1 | sed 's/^-*//')'" 1>&2
      exit 1
      ;;

    *)
      usage
      ;;
  esac
done

Bash prompt

Nerd Fontsの導入で改善したい見た目その①はシェルのプロンプトでした.zshをお使いの方はPowerlevel9kを使えばイケイケなプロンプトになります. 自分はbashのままかfishが多いので,プロンプトをPowerlevel9kっぽく変更したいと思います.試行錯誤の末,完成したプロンプトがこちら.

f:id:takuzoo3868:20181228015533p:plain

  • お決まりのユーザー名・コンピュータ名

  • ディレクトリの種類で表示アイコンを切替

  • git管理ディレクトリ配下ではブランチ名,コミットID,各種ステータス

  • shell err code ステータス

といった表示を行っています.現時刻やipアドレスといった別ツールでも参照しやすい情報は後述するtmuxのステータスラインに表示することにしました. Nerd Fontsを中てた上でbashrcPS1を下記のように上書きすれば同様の表示となるはずです.

#!/usr/bin/env bash

if [[ $COLORTERM = gnome-* && $TERM = xterm ]] && infocmp gnome-256color >/dev/null 2>&1; then
    export TERM='gnome-256color';
elif infocmp xterm-256color >/dev/null 2>&1; then
    export TERM='xterm-256color';
fi;

if tput setaf 1 &> /dev/null; then
    tput sgr0; # reset colors
    bold=$(tput bold);
    reset=$(tput sgr0);

    black=$(tput setaf 234);
    blue=$(tput setaf 27);
    cyan=$(tput setaf 39);
    green=$(tput setaf 76);
    orange=$(tput setaf 166);
    purple=$(tput setaf 125);
    red=$(tput setaf 124);
    violet=$(tput setaf 61);
    white=$(tput setaf 15);
    yellow=$(tput setaf 154);
else
    bold='';
    reset="\e[00m";
    black="\e[1;30m";
    blue="\e[1;34m";
    cyan="\e[1;36m";
    green="\e[1;32m";
    orange="\e[1;33m";
    purple="\e[1;35m";
    red="\e[1;31m";
    violet="\e[1;35m";
    white="\e[1;37m";
    yellow="\e[1;33m";
fi;

export PROMPT_DIRTRIM=2

# icons set
ICON_HOME=""
ICON_DIR=""
ICON_ETC=""
ICON_USER=""
ICON_HOST=""

ICON_OK=""
ICON_FAIL=""
ICON_LOCK=""
ICON_NOT_FOUND=""
ICON_STOP=""

ICON_OCTOCAT=""
ICON_GIT_BITBUCKET=""
ICON_GIT_GITLAB=""

ICON_GIT_BRANCH=""
ICON_GIT_COMMIT=""
ICON_GIT_REMOTE_BRANCH=""
ICON_GIT_UNTRACKED=""
ICON_GIT_UNSTAGED=""
ICON_GIT_STAGED=""
ICON_GIT_STASH=""
ICON_GIT_INCOMING_CHANGES=""
ICON_GIT_OUTGOING_CHANGES=""
ICON_GIT_TAG=""

# git status veiw
prompt_git() {
    local s='';
    local branchName='';
    local gitHash='';

    GIT_DIR="$(git rev-parse --git-dir 2>/dev/null)"

    # Check if the current directory is in a Git repository.
    if [ $(git rev-parse --is-inside-work-tree &>/dev/null; echo "${?}") == '0' ]; then

        # check if the current directory is in .git before running git checks
        if [ "$(git rev-parse --is-inside-git-dir 2> /dev/null)" == 'false' ]; then

            # Ensure the index is up to date.
            git update-index --really-refresh -q &>/dev/null;

            # Check for uncommitted changes in the index.
            if ! $(git diff --quiet --ignore-submodules --cached); then
                s+=${ICON_GIT_STAGED};
            fi;

            # Check for unstaged changes.
            if ! $(git diff-files --quiet --ignore-submodules --); then
                s+=${ICON_GIT_UNSTAGED};
            fi;

            # Check for untracked files.
            if [ -n "$(git ls-files --others --exclude-standard)" ]; then
                s+=${ICON_GIT_UNTRACKED};
            fi;

            # Check for stashed files.
            if $(git rev-parse --verify refs/stash &>/dev/null); then
                s+=${ICON_GIT_STASH};
            fi;

        fi;

        # Get the short symbolic ref.
        # If HEAD isn’t a symbolic ref, get the short SHA for the latest commit
        # Otherwise, just give up.
        branchName=" ${ICON_GIT_BRANCH} $(git symbolic-ref --quiet --short HEAD 2> /dev/null || \
           git rev-parse --short HEAD 2> /dev/null || \
           echo '(unknown)')";

        [ -n "${s}" ] && s=" ${s}";

        # Get commit hash
        gitHash=" ${ICON_GIT_COMMIT} $(git rev-parse --short HEAD)";


        echo -e "${ICON_OCTOCAT}${1}${branchName}${gitHash}${2}${s}";
    else
        return;
    fi;
}

prompt_dir_icon(){
    case $PWD in
        $HOME)
            echo ${ICON_HOME}
            ;;
        "/etc")
            echo ${ICON_ETC}
            ;;
        *) 
            echo ${ICON_DIR}
            ;;
    esac
}

prompt_user(){
    if [[ "${USER}" == "root" ]]; then
        user_state="${orange}";
    else
        user_state="${blue}";
    fi;

    # the hostname when connected via SSH.
    if [[ "${SSH_TTY}" ]]; then
        hostStyle="${bold}${red}";
    else
        hostStyle="${yellow}";
    fi;

    echo -e "${user_state}${ICON_USER}"
}

prompt_host(){
    echo -e "${cyan}${ICON_HOST}"
}

prompt_result() {
  code=$?
  if [ ${code} == 0 ]; then
    echo -e "${ICON_OK}";
  elif [ ${code} == 126 ]; then
    echo -e "${ICON_LOCK}";            # Command invoked cannot execute
  elif [ ${code} == 127 ]; then
    echo -e "${ICON_NOT_FOUND}";       # Command not found
  elif [ ${code} == 130 ]; then
    echo -e "${ICON_STOP}";            # Script terminated by Control-C
  else
    echo -e "${ICON_FAIL} ${bold}${code}${reset}";
  fi;
}

# Set the terminal title and prompt.
PS1=" ";
PS1+="\$(prompt_user) \[${bold}\]\u ";
PS1+="\[${reset}\]";
PS1+="\$(prompt_host) \[${bold}\]\h ";
PS1+="\[${reset}\]";
PS1+="\[${green}\]\$(prompt_dir_icon) \[${bold}\]\w ";
PS1+="\[${reset}\]";
PS1+="\[${yellow}\]\$(prompt_git) ";
PS1+="\[${reset}\]";                                        
PS1+="\[${yellow}\]\$(prompt_result)";
PS1+="\[${reset}\]";                                    
PS1+="\n";
PS1+="\$ ";                            
export PS1;

Tmux status line

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

f:id:takuzoo3868:20181228022707p:plain

メンテナンスしやすいようtmux.conftmux_local.confへ分離したり,色々やってますが長くなるので割愛します.dotfilesにあるこいつを見てくれ!独自にステータスへスクリプトを組み込むことも可能です.自分の場合は例として,local IP / global IP / 天気情報をスクリプトで追加しています.

#!/usr/bin/env bash
#
# Author: takuzoo3868
# Last Modified: 25 Nov 2018.

# Check OS
ostype() { echo $OSTYPE | tr '[A-Z]' '[a-z]'; }

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' || $SHELL_PLATFORM == 'osx' ]]; }

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

__run_lan() {
    if shell_is_bsd || shell_is_osx ; then
        all_nics=$(ifconfig 2>/dev/null | awk -F':' '/^[a-z]/ && !/^lo/ { print $1 }')
        for nic in ${all_nics[@]}; do
            ipv4s_on_nic=$(ifconfig ${nic} 2>/dev/null | awk '$1 == "inet" { print $2 }')
            for lan_ip in ${ipv4s_on_nic[@]}; do
                [[ -n "${lan_ip}" ]] && break
            done
            [[ -n "${lan_ip}" ]] && break
        done
    else
        # Get the names of all attached NICs.
        all_nics="$(ip addr show | cut -d ' ' -f2 | tr -d :)"
        all_nics=(${all_nics[@]//lo/})    # Remove lo interface.

        for nic in "${all_nics[@]}"; do
            # Parse IP address for the NIC.
            lan_ip="$(ip addr show ${nic} | grep '\<inet\>' | tr -s ' ' | cut -d ' ' -f3)"
            # Trim the CIDR suffix.
            lan_ip="${lan_ip%/*}"
            # Only display the last entry
            lan_ip="$(echo "$lan_ip" | tail -1)"

            [ -n "$lan_ip" ] && break
        done
    fi

    echo "${lan_ip-N/a}"
    return 0
}

__run_lan
#!/usr/bin/env bash
#
# Author: takuzoo3868
# Last Modified: 25 Nov 2018.

# Check OS
ostype() { echo $OSTYPE | tr '[A-Z]' '[a-z]'; }

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' || $SHELL_PLATFORM == 'osx' ]]; }

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

__run_wan() {
    local tmp_file="${DIR_TEMPORARY}/wan_ip.txt"
    local wan_ip

    if [ -f "$tmp_file" ]; then
        if shell_is_osx || shell_is_bsd; then
            stat >/dev/null 2>&1 && is_gnu_stat=false || is_gnu_stat=true
            if [ "$is_gnu_stat" == "true" ];then
                last_update=$(stat -c "%Y" ${tmp_file})
            else
                last_update=$(stat -f "%m" ${tmp_file})
            fi
        elif shell_is_linux || [ -z $is_gnu_stat]; then
            last_update=$(stat -c "%Y" ${tmp_file})
        fi

        time_now=$(date +%s)
        update_period=900
        up_to_date=$(echo "(${time_now}-${last_update}) < ${update_period}" | bc)

        if [ "$up_to_date" -eq 1 ]; then
            wan_ip=$(cat ${tmp_file})
        fi
    fi

    if [ -z "$wan_ip" ]; then
        wan_ip=$(curl --max-time 2 -s http://whatismyip.akamai.com/)

        if [ "$?" -eq "0" ]; then
            echo "${wan_ip}" > $tmp_file
        elif [ -f "${tmp_file}" ]; then
            wan_ip=$(cat "$tmp_file")
        fi
    fi

    if [ -n "$wan_ip" ]; then
        echo "${wan_ip}"
    fi

    return 0
}

__run_wan
#!/usr/bin/env bash
#
# Author: takuzoo3868
# Last Modified: 25 Nov 2018.
# API: http://developer.yahoo.com/weather
#
# osx need coreutils

# Check OS
ostype() { echo $OSTYPE | tr '[A-Z]' '[a-z]'; }

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="yahoo"
WEATHER_UNIT_DEFAULT="c"
WEATHER_UPDATE_PERIOD_DEFAULT="600"
# input your woeid https://lab.syncer.jp/Tool/WOEID-Lookup/
WEATHER_LOCATION_DEFAULT="1118108"

export WEATHER_DATA_PROVIDER="${WEATHER_DATA_PROVIDER_DEFAULT}"
# What unit to use. Can be any of {c,f,k}.
export WEATHER_UNIT="${WEATHER_UNIT_DEFAULT}"
# 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_yahoo.txt"
  local weather
  case "$WEATHER_DATA_PROVIDER" in
    "yahoo") weather=$(__yahoo_weather) ;;
    *)
      echo "Unknown weather provider [${$WEATHER_DATA_PROVIDER}]";
      return 1
  esac
  if [ -n "$weather" ]; then
    echo "$weather"
  fi
}

# Get the weather from Yahoo!
__yahoo_weather() {
  degree=""
  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

  if [ -z "$degree" ]; then
    weather_data=$(curl --max-time 4 -s "https://query.yahooapis.com/v1/public/yql?format=xml&q=SELECT%20*%20FROM%20weather.forecast%20WHERE%20u=%27${WEATHER_UNIT}%27%20AND%20woeid%20=%20%27${WEATHER_LOCATION}%27")
    if [ "$?" -eq "0" ]; then
      error=$(echo "$weather_data" | grep "problem_cause\|DOCTYPE");
      if [ -n "$error" ]; then
        echo "error"
        exit 1
      fi

      # Assume latest grep is in PATH
      gnugrep="${WEATHER_GREP}"

      # <yweather:units temperature="F" distance="mi" pressure="in" speed="mph"/>
      unit=$(echo "$weather_data" | "$gnugrep" -Zo "<yweather:units [^<>]*/>" | sed 's/.*temperature="\([^"]*\)".*/\1/')
      condition=$(echo "$weather_data" | "$gnugrep" -Zo "<yweather:condition [^<>]*/>")
      # <yweather:condition  text="Clear"  code="31"  temp="66"  date="Mon, 01 Oct 2012 8:00 pm CST" />
      degree=$(echo "$condition" | sed 's/.*temp="\([^"]*\)".*/\1/')
      condition=$(echo "$condition" | sed 's/.*text="\([^"]*\)".*/\1/')
      # Pull the times for sunrise and sunset so we know when to change the day/night indicator
      # <yweather:astronomy sunrise="6:56 am"   sunset="6:21 pm"/>
      if shell_is_bsd; then
        date_arg='-j -f "%H:%M %p "'
      else
        date_arg='-d'
      fi
      sunrise=$(date ${date_arg}"$(echo "$weather_data" | "$gnugrep" "yweather:astronomy" | sed 's/^\(.*\)sunset.*/\1/' | sed 's/^.*sunrise="\(.*m\)".*/\1/')" +%H%M)
      sunset=$(date ${date_arg}"$(echo "$weather_data" | "$gnugrep" "yweather:astronomy" | sed 's/^.*sunset="\(.*m\)".*/\1/')" +%H%M)
    elif [ -f "${tmp_file}" ]; then
      __read_tmp_file
    fi
  fi

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

# Get symbol for condition. 
# Available conditions: http://developer.yahoo.com/weather/#codes
__get_weather_image() {
  local condition=$(echo "$1" | tr '[:upper:]' '[:lower:]')
  local sunrise="$2"
  local sunset="$3"
  case "$condition" in
    "tornado" | "tropical storm" | "hurricane")
      echo "" # weather_hurricane
      ;;
    "sunny" | "fair")
      time_forecast=$(date +%H%M)
      if [ "$time_forecast" -ge "$sunset" -o "$time_forecast" -le "$sunrise" ]; then
        echo ""
      else
        echo "" # mdi_weather_sunny
      fi
      ;;
    "hot")
      time_forecast=$(date +%H%M)
      if [ "$time_forecast" -ge "$sunset" -o "$time_forecast" -le "$sunrise" ]; then
        echo ""
      else
        echo "" # weather_hot
      fi
      ;;
    "rain" | "light rain" | "drizzle" | "light drizzle")
      echo ""
      ;;
    "showers" | "scattered showers")
      echo "" # weather_showers
      ;;
    "mixed rain and snow" | "mixed rain and sleet" | "freezing drizzle" | "freezing rain" | "mixed rain and hail" | "rain and snow")
      echo "" # weather_rain_mix
      ;;
    "light rain with thunder")
      echo "" # mdi_weather_lightning
      ;;
    "snow" | "mixed snow and sleet" | "snow flurries" | "light snow showers" | "blowing snow" | "sleet" | "heavy snow" | "scattered snow showers" | "snow showers" | "light snow" | "snow grains")
      echo ""
      ;;
    "hail")
      echo "" # mdi_weather_hail
      ;;
    "cloudy" | "mostly cloudy")
      echo "" # fa_cloud
      ;;
    "partly cloudy")
      echo "" # mdi_weather_partlycloudy
      ;;
    "severe thunderstorms" | "thunderstorms" | "isolated thunderstorms" | "scattered thunderstorms" | "isolated thundershowers" | "thundershowers")
      echo "" # weather_lightning
      ;;
    "dust" | "fog" | "haze" | "smoky")
      echo "" # weather_dust
      ;;
    "fog" | "foggy" | "mist")
      echo "" # weather_fog
      ;;
    "windy" | "blustery" | "breezy")
      echo "" # mdi_weather_windy
      ;;
    "clear" | "cold")
      time_forecast=$(date +%H%M)
      if [ "$time_forecast" -ge "$sunset" -o "$time_forecast" -le "$sunrise" ]; then
        echo "" # mdi_weather_night
      else
        echo ""
      fi
      ;;
    "not available")
      echo "" 
      ;;
    *)
      echo "" # unknown
      ;;
  esac
}

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

# exec
__run_weather

特に天気情報を表示するスクリプトは更新の止まっているプラグインが多かったので,書いてみて勉強にもなりました.

おわりに

今までの一連の設定を終えるとこんな感じにCUI作業環境が整います.

これらの環境で必要な設定ファイルは全て自分のdotfilesに入っていますので良かったら使ってみてください.

github.com

今後はタスク管理やメール管理もCUIで整備したいと考えています.ちなみにIRCtwitter環境やslackのTUI版であるsclackの導入も考えましたが,うまく出来なかったので知見ある方はコメントで教えていただけると助かります!それでは良きCUIライフを!

*1:筆者はこの他にMOONGIFTさんの記事をチェックしています.