diff --git a/scripts/common.sh b/scripts/common.sh index 7539e9f5..824f7d99 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -328,9 +328,10 @@ ctms_require_dependencies() { fi } -# ── 长命令执行(折叠输出 + spinner) ────────── +# ── 长命令执行(默认 spinner,可选实时日志窗口) ── # 用法: ctms_run_quiet <提示语> -- <命令...> -# VERBOSE=1 时展开实时输出;否则折叠为 spinner,失败时回放完整日志。 +# CTMS_LIVE_OUTPUT=1 时显示最近几行;VERBOSE=1 时展开全部实时输出。 +# 其他情况仅显示 spinner,失败时回放完整日志。 ctms_run_quiet() { local label="$1"; shift [[ "${1:-}" == "--" ]] && shift @@ -351,13 +352,21 @@ ctms_run_quiet() { if [[ -t 1 ]]; then "$@" >"$log_file" 2>&1 & local cmd_pid=$! - _ctms_spinner "$cmd_pid" "$label" "$start_ts" & - local spin_pid=$! + if [[ "${CTMS_LIVE_OUTPUT:-0}" -eq 1 ]]; then + _ctms_live_log "$cmd_pid" "$label" "$start_ts" "$log_file" & + else + _ctms_spinner "$cmd_pid" "$label" "$start_ts" & + fi + local display_pid=$! wait "$cmd_pid" exit_code=$? - kill "$spin_pid" 2>/dev/null || true - wait "$spin_pid" 2>/dev/null || true - printf '\r\033[K' + kill "$display_pid" 2>/dev/null || true + wait "$display_pid" 2>/dev/null || true + if [[ "${CTMS_LIVE_OUTPUT:-0}" -eq 1 ]]; then + _ctms_clear_live_log + else + printf '\r\033[K' + fi else printf ' %s%s%s %s …\n' "${CC_MUTED}" "$ICON_INFO" "${C_RESET}" "$label" "$@" >"$log_file" 2>&1 @@ -379,12 +388,17 @@ ctms_run_quiet() { ctms_fail "命令执行失败,详细日志保留于:${log_file#"$CTMS_ROOT_DIR"/}(追加 --verbose 可看实时输出)" } +# 固定占用 9 行:上边框 + 6 行日志 + 下边框 + 状态行。 +# 日志行会截断,避免终端自动换行破坏 ANSI 光标定位。 +_CTMS_LIVE_LOG_ROWS=6 +_CTMS_LIVE_LOG_HEIGHT=9 + _ctms_spinner() { local cmd_pid="$1" label="$2" start_ts="$3" local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') - local i=0 + local i=0 elapsed while kill -0 "$cmd_pid" 2>/dev/null; do - local elapsed=$(( $(date +%s) - start_ts )) + elapsed=$(( $(date +%s) - start_ts )) printf '\r\033[K %s%s%s %s %s(%ss)%s' \ "${CC_INFO}${C_BOLD}" "${frames[i]}" "${C_RESET}" \ "$label" "${CC_MUTED}" "$elapsed" "${C_RESET}" @@ -393,6 +407,82 @@ _ctms_spinner() { done } +_ctms_live_log() { + local cmd_pid="$1" label="$2" start_ts="$3" log_file="$4" + local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') + local i=0 first=1 + local columns box_width content_width elapsed line row + local -a lines=() + + columns="$(tput cols 2>/dev/null || printf 80)" + [[ "$columns" =~ ^[0-9]+$ ]] || columns=80 + box_width=$(( columns - 4 )) + [[ "$box_width" -gt 100 ]] && box_width=100 + [[ "$box_width" -lt 50 ]] && box_width=50 + content_width=$(( box_width - 4 )) + + while kill -0 "$cmd_pid" 2>/dev/null; do + lines=() + while IFS= read -r line; do + lines+=("$line") + done < <(_ctms_recent_log_lines "$log_file" "$_CTMS_LIVE_LOG_ROWS" "$content_width") + + if [[ "$first" -eq 0 ]]; then + printf '\r\033[%dA' "$(( _CTMS_LIVE_LOG_HEIGHT - 1 ))" + fi + + printf '\033[2K %s╭─ 实时输出 %s╮%s\n' \ + "${CC_PRIMARY}${C_BOLD}" "$(ctms_repeat_char '─' "$(( box_width - 14 ))")" "${C_RESET}" + for ((row=0; row<_CTMS_LIVE_LOG_ROWS; row++)); do + line="${lines[$row]:-}" + printf '\033[2K %s│%s %-*s %s│%s\n' \ + "${CC_PRIMARY}" "${C_RESET}${CC_MUTED}" "$content_width" "$line" "${CC_PRIMARY}" "${C_RESET}" + done + printf '\033[2K %s╰%s╯%s\n' \ + "${CC_PRIMARY}" "$(ctms_repeat_char '─' "$(( box_width - 2 ))")" "${C_RESET}" + elapsed=$(( $(date +%s) - start_ts )) + printf '\033[2K %s%s%s %s %s(%ss)%s' \ + "${CC_INFO}${C_BOLD}" "${frames[i]}" "${C_RESET}" \ + "$label" "${CC_MUTED}" "$elapsed" "${C_RESET}" + + first=0 + i=$(( (i + 1) % ${#frames[@]} )) + sleep 0.2 + done +} + +_ctms_recent_log_lines() { + local log_file="$1" rows="$2" width="$3" + [[ -s "$log_file" ]] || return 0 + + if [[ "$_CTMS_HAS_PERL" -eq 1 ]]; then + # 只读取文件尾部,清除颜色/光标控制符,并把 Docker 的 CR 进度输出拆成独立行。 + tail -n 80 "$log_file" | perl -CSDA -pe ' + s/\e\[[0-9;?]*[ -\/]*[@-~]//g; + s/\e[()][0-9A-Za-z]//g; + s/\r/\n/g; + ' | tail -n "$rows" | perl -CSDA -Mutf8 -pe \ + 'BEGIN { $width = shift @ARGV } chomp; $_ = substr($_, 0, $width) . "\n";' "$width" + else + local esc + esc="$(printf '\033')" + tail -n 80 "$log_file" \ + | tr '\r' '\n' \ + | sed -e "s/${esc}\\[[0-9;?]*[A-Za-z]//g" -e "s/${esc}[()][0-9A-Za-z]//g" \ + | tail -n "$rows" \ + | cut -c "1-${width}" + fi +} + +_ctms_clear_live_log() { + printf '\r\033[%dA' "$(( _CTMS_LIVE_LOG_HEIGHT - 1 ))" + local row + for ((row=0; row<_CTMS_LIVE_LOG_HEIGHT-1; row++)); do + printf '\033[2K\n' + done + printf '\033[2K\033[%dA\r' "$(( _CTMS_LIVE_LOG_HEIGHT - 1 ))" +} + # ── .env 读写 ───────────────────────────────── ctms_read_env_value() { local key="$1" diff --git a/scripts/install.sh b/scripts/install.sh index 0a1cf80e..0e2aedb5 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -339,9 +339,9 @@ run_build_and_start() { else step "构建镜像并启动服务" if [[ "$TARGET_ENV" == "dev" ]]; then - ctms_run_quiet "构建并刷新开发容器(首次较慢)" -- compose_cmd up -d --build --force-recreate backend nginx frontend-dev + CTMS_LIVE_OUTPUT=1 ctms_run_quiet "构建并刷新开发容器(首次较慢)" -- compose_cmd up -d --build --force-recreate backend nginx frontend-dev else - ctms_run_quiet "构建镜像并启动容器(首次较慢)" -- compose_cmd up -d --build + CTMS_LIVE_OUTPUT=1 ctms_run_quiet "构建镜像并启动容器(首次较慢)" -- compose_cmd up -d --build fi fi }