169 lines
5.4 KiB
C
169 lines
5.4 KiB
C
/* spinner.c - "思考中"指示器实现 */
|
|
#include "spinner.h"
|
|
#include "i18n.h"
|
|
|
|
#include <pthread.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.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);
|
|
}
|