Files
ligent/src/spinner.c
T
2026-10-03 14:56:56 +08:00

166 lines
5.4 KiB
C

/* spinner.c - "思考中"指示器实现 */
#include "spinner.h"
#include "i18n.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>
#define DIM "\033[2m"
#define OFF "\033[0m"
static pthread_t g_thread;
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_cond;
static pthread_once_t g_cond_once = PTHREAD_ONCE_INIT;
static volatile int g_running;
static int g_started;
static int g_enabled;
static char g_label[64];
/* 当前这段等待的起点. 换标签时重置, 于是秒数反映的是"这个阶段等了多久":
* 先看到"等待响应 (1s)", 切成"思考中"后重新从 0 数起. */
static struct timespec g_t0;
/* 条件变量必须显式指定用 CLOCK_MONOTONIC.
*
* pthread_cond_timedwait 默认按 CLOCK_REALTIME 解释超时, 而我们算出来的
* 是 CLOCK_MONOTONIC 的绝对时间. 开机时长 (几十万秒)永远小于 epoch 时间
* (十几亿秒), 于是"超时时刻"永远被当成已经过去 —— timedwait 立刻返回,
* worker 变成疯狂重绘的忙循环. */
static void init_cond(void)
{
pthread_condattr_t attr;
pthread_condattr_init(&attr);
pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
pthread_cond_init(&g_cond, &attr);
pthread_condattr_destroy(&attr);
}
static void *worker(void *arg)
{
(void)arg;
static const char *dots[] = { ".", "..", "...", "...." };
int i = 0;
struct timespec next;
clock_gettime(CLOCK_MONOTONIC, &next);
for (;;) {
struct timespec now;
long elapsed;
pthread_mutex_lock(&g_lock);
if (!g_running) { pthread_mutex_unlock(&g_lock); break; }
clock_gettime(CLOCK_MONOTONIC, &now);
elapsed = (long)(now.tv_sec - g_t0.tv_sec);
fprintf(stderr, "\r\033[K%s%s%s%s", DIM, g_label, dots[i], OFF);
/* 等得越久越该说明"还活着", 超过 5 秒就把秒数亮出来 */
if (elapsed >= 5) fprintf(stderr, " %s(%lds)%s", DIM, elapsed, OFF);
fflush(stderr);
i = (i + 1) % 4;
/* 睡到下一帧. 这里必须用条件变量定时等, 不能 nanosleep:
* spinner_stop() 要 pthread_join, nanosleep 的话就得干等这一觉
* 睡满 —— 每次收尾白搭最多 250ms, 而这笔账直接加在用户感知的
* 延迟上 (每轮模型往返、每个工具来回都要付一次). */
next.tv_nsec += 250000000L;
if (next.tv_nsec >= 1000000000L) {
next.tv_sec++;
next.tv_nsec -= 1000000000L;
}
pthread_cond_timedwait(&g_cond, &g_lock, &next);
pthread_mutex_unlock(&g_lock);
}
return NULL;
}
void spinner_init(int enabled)
{
g_enabled = enabled ? 1 : 0;
}
/* 调用方必须已经持有锁. 标签真的变了才重置计时,
* 否则每来一片思维链都重置, "思考中 (12s)"永远停在 0. */
static void set_label_locked(const char *label)
{
const char *want = (label && *label) ? label : t("spinner.thinking");
if (strcmp(g_label, want) == 0) return;
snprintf(g_label, sizeof g_label, "%s", want);
clock_gettime(CLOCK_MONOTONIC, &g_t0);
}
void spinner_start(const char *label)
{
if (!g_enabled) return;
pthread_once(&g_cond_once, init_cond); /* 保证 g_cond 用的是单调时钟 */
pthread_mutex_lock(&g_lock);
if (!g_started) {
/* 先把标签算成**一份副本**再写进 g_label: label 有可能就是 g_label 自己
* —— spinner_notice() 从一条重试提示里恢复动画时正是这么调的. 以前这里
* 先执行 g_label[0] = '\0', 于是要读的那个字符串被自己清空, 标签退化成
* 默认的"思考中": 每次重试退避之后, 屏幕上的"等待响应"都会变成"思考中",
* 而这两段等待的含义恰恰是它要区分的. */
char want[sizeof g_label];
snprintf(want, sizeof want, "%s",
(label && *label) ? label : t("spinner.thinking"));
g_label[0] = '\0'; /* 让 set_label_locked 一定认成"变了" */
set_label_locked(want); /* 传副本, 不传 g_label 自己 */
g_running = 1;
g_started = 1;
if (pthread_create(&g_thread, NULL, worker, NULL) != 0) {
g_started = 0;
g_running = 0;
}
}
pthread_mutex_unlock(&g_lock);
}
void spinner_set_label(const char *label)
{
if (!g_enabled) return;
pthread_mutex_lock(&g_lock);
set_label_locked(label);
pthread_mutex_unlock(&g_lock);
}
void spinner_stop(void)
{
if (!g_enabled) return;
pthread_mutex_lock(&g_lock);
if (!g_started) { pthread_mutex_unlock(&g_lock); return; }
g_running = 0;
pthread_cond_signal(&g_cond); /* 立刻叫醒 worker, 别等它睡满一帧 */
pthread_mutex_unlock(&g_lock);
pthread_join(g_thread, NULL);
pthread_mutex_lock(&g_lock);
g_started = 0;
fputs("\r\033[K", stderr); /* 把动画那一行擦干净 */
fflush(stderr);
pthread_mutex_unlock(&g_lock);
}
void spinner_notice(const char *fmt, ...)
{
int was_on;
pthread_mutex_lock(&g_lock);
was_on = g_started;
pthread_mutex_unlock(&g_lock);
if (was_on) spinner_stop();
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
fflush(stderr);
if (was_on) spinner_start(g_label);
}