nekolas note
4.7 KB Raw
#!/usr/bin/env sh
# cc-tmux ランタイム状態ストア(T005)。
# 参照: contracts/state-store.md, data-model.md
#
# 場所: ${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/cc-tmux/<session_id>/
#   ├── meta         KEY=VALUE: tmux_pane, window_id, name, name_locked, cwd
#   ├── state        "busy" | "idle"
#   └── spinner.pid  スピナーループの pid(任意)
#
# 真実の所在: state はこのストア、表示の真実は tmux ユーザーオプション(同期は各イベントで)。
# 書き込みはアトミック(tmp へ書いて mv)。
#
# このファイルは bin/* から `. lib/state.sh` で読み込まれる(POSIX sh 互換)。

# ストアのベースディレクトリ。
cc_state_base() {
  printf '%s/cc-tmux' "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}"
}

# 指定セッションのディレクトリパスを出力。
cc_state_dir() {
  printf '%s/%s' "$(cc_state_base)" "$1"
}

# アトミック書き込み: cc_atomic_write <path> <content>
cc_atomic_write() {
  _aw_path=$1
  _aw_dir=$(dirname "$_aw_path")
  [ -d "$_aw_dir" ] || mkdir -p "$_aw_dir" 2>/dev/null || return 1
  _aw_tmp="$_aw_path.tmp.$$"
  printf '%s' "$2" > "$_aw_tmp" 2>/dev/null || return 1
  mv -f "$_aw_tmp" "$_aw_path" 2>/dev/null || { rm -f "$_aw_tmp" 2>/dev/null; return 1; }
  return 0
}

# state_init <session_id>
#   ディレクトリを用意し state を idle で初期化(既存 state は保持)。
#   meta は state_meta_set で別途記録する。
state_init() {
  [ -n "${1:-}" ] || return 1
  _si_dir=$(cc_state_dir "$1")
  mkdir -p "$_si_dir" 2>/dev/null || return 1
  # state ファイルが無ければ idle で作る(compact 明けの busy 維持は呼び出し側が制御)。
  [ -f "$_si_dir/state" ] || cc_atomic_write "$_si_dir/state" "idle"
  return 0
}

# state_set <session_id> <busy|idle>
state_set() {
  [ -n "${1:-}" ] || return 1
  case "${2:-}" in
    busy|idle) : ;;
    *) return 1 ;;
  esac
  _ss_dir=$(cc_state_dir "$1")
  mkdir -p "$_ss_dir" 2>/dev/null || return 1
  cc_atomic_write "$_ss_dir/state" "$2"
}

# state_get <session_id> -> "busy"|"idle"|""(未知)
state_get() {
  [ -n "${1:-}" ] || return 1
  _sg_file="$(cc_state_dir "$1")/state"
  [ -f "$_sg_file" ] || return 1
  cat "$_sg_file" 2>/dev/null
}

# state_meta_set <session_id> <key> <value>
#   meta ファイルへ KEY=VALUE を upsert(アトミック)。値の改行は不可。
state_meta_set() {
  [ -n "${1:-}" ] && [ -n "${2:-}" ] || return 1
  _sms_dir=$(cc_state_dir "$1")
  mkdir -p "$_sms_dir" 2>/dev/null || return 1
  _sms_file="$_sms_dir/meta"
  _sms_key=$2
  _sms_val=$3
  _sms_new=""
  _sms_found=0
  if [ -f "$_sms_file" ]; then
    # 既存行を読み、同一キーは置換。
    while IFS= read -r _line || [ -n "$_line" ]; do
      case "$_line" in
        "$_sms_key="*)
          _sms_new="$_sms_new$_sms_key=$_sms_val
"
          _sms_found=1
          ;;
        *)
          _sms_new="$_sms_new$_line
"
          ;;
      esac
    done < "$_sms_file"
  fi
  if [ "$_sms_found" -eq 0 ]; then
    _sms_new="$_sms_new$_sms_key=$_sms_val
"
  fi
  cc_atomic_write "$_sms_file" "$_sms_new"
}

# state_meta <session_id> <key> -> 値(無ければ空・exit 1)
state_meta() {
  [ -n "${1:-}" ] && [ -n "${2:-}" ] || return 1
  _sm_file="$(cc_state_dir "$1")/meta"
  [ -f "$_sm_file" ] || return 1
  # 最初に一致したキーの値を返す。
  while IFS= read -r _line || [ -n "$_line" ]; do
    case "$_line" in
      "$2="*)
        printf '%s' "${_line#*=}"
        return 0
        ;;
    esac
  done < "$_sm_file"
  return 1
}

# state_cleanup <session_id>
#   スピナーを停止し、ディレクトリを削除する。
state_cleanup() {
  [ -n "${1:-}" ] || return 1
  _sc_dir=$(cc_state_dir "$1")
  if [ -f "$_sc_dir/spinner.pid" ]; then
    _sc_pid=$(cat "$_sc_dir/spinner.pid" 2>/dev/null)
    if [ -n "$_sc_pid" ]; then
      kill "$_sc_pid" 2>/dev/null
    fi
  fi
  rm -rf "$_sc_dir" 2>/dev/null
  return 0
}

# state_cleanup_stale_pane <pane>
#   同一 pane を参照する古いセッションディレクトリを掃除(クラッシュ後の孤児対策)。
#   引数 pane に一致し、かつ現在のセッションでない entry を削除する。
#   呼び出し側で keep する session_id を第2引数に渡す。
state_cleanup_stale_pane() {
  _scsp_pane=${1:-}
  _scsp_keep=${2:-}
  [ -n "$_scsp_pane" ] || return 0
  _scsp_base=$(cc_state_base)
  [ -d "$_scsp_base" ] || return 0
  for _d in "$_scsp_base"/*/; do
    [ -d "$_d" ] || continue
    _sid=$(basename "$_d")
    [ "$_sid" = "$_scsp_keep" ] && continue
    _p=$(state_meta "$_sid" tmux_pane 2>/dev/null) || _p=""
    if [ "$_p" = "$_scsp_pane" ]; then
      state_cleanup "$_sid"
    fi
  done
  return 0
}