#!/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
}