@cxmyu/ccglance 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,18 @@ model · effort · context · cache · git · session — all at a glance
19
19
 
20
20
  **Docs:** [Overview](./docs/overview.md) · [Segments & icons](./docs/segments.md) · [Development](./docs/development.md)
21
21
 
22
+ ## Terminal Font
23
+
24
+ Use a Nerd Font in your terminal so emoji, Nerd Font icons, arrows, and box-like
25
+ symbols keep consistent width. [Maple Mono NF](https://github.com/subframe7536/maple-font)
26
+ is a good default; use `Maple Mono NF CN` if you want better CJK coverage.
27
+
28
+ ccglance intentionally outputs standard Unicode emoji. A Nerd Font improves text
29
+ metrics and symbol coverage, but color emoji are still rendered by the operating
30
+ system's emoji font (for example Segoe UI Emoji on Windows, Apple Color Emoji on
31
+ macOS, and Noto Color Emoji on many Linux desktops), so their artwork can differ
32
+ across platforms.
33
+
22
34
  ## Install
23
35
 
24
36
  ### Option A — npm (global, recommended)
@@ -102,12 +114,9 @@ On Windows use a full path, e.g. `node D:\\path\\to\\ccglance\\dist\\cli.js`.
102
114
  > command needs no path at all. `"padding": 0` drops Claude Code's default leading
103
115
  > indent so the row starts at the left edge.
104
116
 
105
- > **Windows latency tip:** the global `ccglance` command is an npm `.cmd` shim,
106
- > which adds a cmd batch layer plus a PATH lookup on every redraw — tens to
107
- > hundreds of milliseconds depending on antivirus and system cache state. For
108
- > the lowest latency, point `command` directly at the compiled file:
109
- > `node D:\\absolute\\path\\to\\ccglance\\dist\\cli.js` (a global install lives
110
- > at `<npm global dir>\\node_modules\\@cxmyu\\ccglance\\dist\\cli.js`).
117
+ > **Windows tip:** if status-line startup latency matters, point `command`
118
+ > directly at the compiled file (`node D:\\absolute\\path\\to\\ccglance\\dist\\cli.js`)
119
+ > instead of the global `ccglance` command — it usually starts a bit faster.
111
120
 
112
121
  ## Contributing
113
122
 
package/README.zh-CN.md CHANGED
@@ -19,6 +19,16 @@
19
19
 
20
20
  **文档:** [概览](./docs/overview.zh-CN.md) · [字段与图标](./docs/segments.zh-CN.md) · [开发](./docs/development.zh-CN.md)
21
21
 
22
+ ## 终端字体
23
+
24
+ 终端建议使用 Nerd Font,避免 emoji、Nerd Font 图标、箭头和类进度符号宽度不一致。
25
+ 首选可以用 [Maple Mono NF](https://github.com/subframe7536/maple-font);
26
+ 需要更好的中文显示时用 `Maple Mono NF CN`。
27
+
28
+ ccglance 会固定输出标准 Unicode emoji。Nerd Font 可以改善文本宽度和符号覆盖,
29
+ 但彩色 emoji 仍由系统 emoji 字体渲染(例如 Windows 的 Segoe UI Emoji、
30
+ macOS 的 Apple Color Emoji、很多 Linux 桌面的 Noto Color Emoji),所以不同平台的图案可能不同。
31
+
22
32
  ## 安装
23
33
 
24
34
  ### 方式 A —— npm 全局安装(推荐)
@@ -97,10 +107,8 @@ Windows 用完整路径,例如 `node D:\\path\\to\\ccglance\\dist\\cli.js`。
97
107
  > 全局的 `ccglance` 命令则完全不需要写路径。`"padding": 0` 去掉 Claude Code 默认的左侧缩进,
98
108
  > 让状态栏从最左开始。
99
109
 
100
- > **Windows 延时提示**:全局命令 `ccglance` 实际是 npm 的 `.cmd` shim,每次重绘要多走一层
101
- > cmd 批处理 + PATH 查找,视杀软/系统缓存状态可能多几十到上百毫秒。追求最低延时时,
102
- > 建议 `command` 直接写 `node D:\\绝对路径\\ccglance\\dist\\cli.js`(全局安装的包在
103
- > `<npm 全局目录>\\node_modules\\@cxmyu\\ccglance\\dist\\cli.js`)。
110
+ > **Windows 提速小贴士**:若在意状态栏启动延时,可把 `command` 直接指向编译产物
111
+ > `node D:\\绝对路径\\ccglance\\dist\\cli.js`,通常比走全局 `ccglance` 命令更快一些。
104
112
 
105
113
  ## 贡献
106
114
 
@@ -4,19 +4,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  // 后台刷新 helper —— 由 refresh.ts 以 detached 进程启动,绝不被状态栏进程等待。
7
- // 任务列表从 argv[2] 指向的 task-*.json 读入,全部尽力而为、静默失败:
7
+ // 任务列表从 argv[2](JSON)读入,全部尽力而为、静默失败:
8
8
  // git —— 单次有界 `git status --porcelain=v2 --branch`,解析后原子写缓存
9
9
  // version —— 拉 npm registry 的 Claude Code 最新版本,原子写缓存
10
10
  // 每个任务无论成败都要移除自己的 .refresh marker,否则只能等节流窗口过期。
11
+ // 每次运行还顺手清理孤儿产物:过期的 .refresh / .tmp(及历史遗留的 tasks/ 目录)。
11
12
  const fs_1 = __importDefault(require("fs"));
12
13
  const path_1 = __importDefault(require("path"));
13
14
  const paths_1 = require("../utils/paths");
14
15
  const GIT_TIMEOUT_MS = 700;
15
- // 缓存清理:每天最多一次(stamp 节流)。缓存 key 是仓库/transcript 路径的哈希,
16
- // 临时目录一去不返时对应 json 便成死条目,按 mtime 过期删除(活跃条目会被刷新持续续命)。
16
+ // 孤儿瞬时产物阈值:.refresh/.tmp 本应几秒内被删(helper 最长约 5s);超过此值即视为
17
+ // 夭折进程的残留。每次运行开头 + 结尾各清一次,故取较短的 2 分钟让残留更快被接管清理。
18
+ const STALE_TRANSIENT_MS = 2 * 60 * 1000;
19
+ // 死缓存清理:缓存 key 是仓库/transcript 路径哈希,临时目录一去不返时对应 json 便成死条目。
20
+ // 每天最多清一次(stamp 节流),删除长期未刷新的缓存 json(活跃条目会被刷新持续续命)。
17
21
  const PRUNE_INTERVAL_MS = 24 * 3600 * 1000;
18
22
  const PRUNE_MAX_AGE_MS = 30 * 24 * 3600 * 1000;
19
- const PRUNE_TRANSIENT_MAX_AGE_MS = 3600 * 1000; // 残留 .refresh 标记 / .tmp 半成品 / task 文件
20
23
  // git status --porcelain=v2 --branch 输出解析(与渲染端 GitCache 结构保持一致)。
21
24
  function parseStatus(out) {
22
25
  let head = '';
@@ -123,9 +126,40 @@ function runVersionTask(cacheFile, done) {
123
126
  finish('');
124
127
  }
125
128
  }
126
- // 过期清理:正常缓存 json 超过 30 天未被刷新(mtime)视为死条目;
127
- // .refresh / .tmp / tasks 超过 1 小时视为进程夭折的残留。逐文件尽力而为。
128
- function pruneCaches(nowMs = Date.now()) {
129
+ // 每次运行都做的轻清理:删除过期的孤儿瞬时产物(.refresh / .tmp),以及历史遗留的
130
+ // tasks/ 目录内容。成本很低(这些目录文件很少),能让残留不再累积。
131
+ function pruneStaleTransient(nowMs = Date.now()) {
132
+ const root = (0, paths_1.appCacheDir)();
133
+ for (const sub of ['git', 'version', 'transcript', 'tasks']) {
134
+ const dir = path_1.default.join(root, sub);
135
+ let names;
136
+ try {
137
+ names = fs_1.default.readdirSync(dir);
138
+ }
139
+ catch {
140
+ continue;
141
+ }
142
+ for (const name of names) {
143
+ // tasks/ 已弃用:其中任何文件都算历史遗留;其他目录只清 .refresh / .tmp 瞬时产物。
144
+ if (sub !== 'tasks' && !name.endsWith('.refresh') && !name.endsWith('.tmp'))
145
+ continue;
146
+ const file = path_1.default.join(dir, name);
147
+ try {
148
+ if (nowMs - fs_1.default.statSync(file).mtimeMs > STALE_TRANSIENT_MS)
149
+ fs_1.default.rmSync(file, { force: true });
150
+ }
151
+ catch { /* ignore */ }
152
+ }
153
+ if (sub === 'tasks') {
154
+ try {
155
+ fs_1.default.rmdirSync(dir);
156
+ }
157
+ catch { /* 非空/不存在都忽略 */ }
158
+ }
159
+ }
160
+ }
161
+ // 每天最多一次(stamp 节流):删除长期未刷新的死缓存 json。
162
+ function pruneDeadCaches(nowMs = Date.now()) {
129
163
  const root = (0, paths_1.appCacheDir)();
130
164
  const stamp = path_1.default.join(root, 'prune.stamp');
131
165
  try {
@@ -140,7 +174,7 @@ function pruneCaches(nowMs = Date.now()) {
140
174
  catch {
141
175
  return;
142
176
  }
143
- for (const sub of ['git', 'transcript', 'version', 'tasks']) {
177
+ for (const sub of ['git', 'transcript', 'version']) {
144
178
  const dir = path_1.default.join(root, sub);
145
179
  let names;
146
180
  try {
@@ -150,26 +184,27 @@ function pruneCaches(nowMs = Date.now()) {
150
184
  continue;
151
185
  }
152
186
  for (const name of names) {
187
+ if (!name.endsWith('.json'))
188
+ continue;
153
189
  const file = path_1.default.join(dir, name);
154
190
  try {
155
- const st = fs_1.default.statSync(file);
156
- if (!st.isFile())
157
- continue;
158
- const age = nowMs - st.mtimeMs;
159
- const transient = sub === 'tasks' || name.endsWith('.refresh') || name.endsWith('.tmp');
160
- if (age > (transient ? PRUNE_TRANSIENT_MAX_AGE_MS : PRUNE_MAX_AGE_MS)) {
191
+ if (nowMs - fs_1.default.statSync(file).mtimeMs > PRUNE_MAX_AGE_MS)
161
192
  fs_1.default.rmSync(file, { force: true });
162
- }
163
193
  }
164
194
  catch { /* ignore */ }
165
195
  }
166
196
  }
167
197
  }
168
198
  function main() {
169
- const taskFile = process.argv[2] || '';
199
+ const payload = process.argv[2] || '';
200
+ // 开头先接管清理上一轮遗留的孤儿(含被强杀进程没来得及删的 marker):即便本次任务中途
201
+ // 被杀、finally 没跑到,这些残留也会在下一次后台刷新一启动时被清掉,不会长期堆积。
170
202
  try {
171
- // 任务文件已由 refresh.ts 原子写入;读损坏时 markers 交给节流窗口 + prune 兜底。
172
- const tasks = taskFile ? JSON.parse(fs_1.default.readFileSync(taskFile, 'utf8')) : [];
203
+ pruneStaleTransient();
204
+ }
205
+ catch { /* best effort */ }
206
+ try {
207
+ const tasks = payload ? JSON.parse(payload) : [];
173
208
  for (const task of tasks) {
174
209
  if (task.kind === 'git') {
175
210
  try {
@@ -189,17 +224,15 @@ function main() {
189
224
  }
190
225
  }
191
226
  catch {
192
- // 任务文件缺失 / 损坏:静默退出,仍走 finally 清理。
227
+ // payload 缺失 / 损坏:静默退出,仍走 finally 清理。
193
228
  }
194
229
  finally {
195
- if (taskFile) {
196
- try {
197
- fs_1.default.rmSync(taskFile, { force: true });
198
- }
199
- catch { /* ignore */ }
230
+ try {
231
+ pruneStaleTransient();
200
232
  }
233
+ catch { /* best effort */ }
201
234
  try {
202
- pruneCaches();
235
+ pruneDeadCaches();
203
236
  }
204
237
  catch { /* best effort */ }
205
238
  }
@@ -6,16 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.queueRefreshTask = queueRefreshTask;
7
7
  exports.flushRefreshTasks = flushRefreshTasks;
8
8
  // 后台刷新调度 —— 渲染期间各段只「入队」(调用方先抢占各自的 .refresh marker),
9
- // stdout 写出后由 cli 调 flushRefreshTasks() 一次性 spawn 单个 detached helper(bg.js)。
10
- //
11
- // 为什么不各自直接 spawn:Windows 上发起一个 node.exe 子进程的同步成本很高
12
- // (CreateProcess + 杀软扫描,实测 ~80-110ms),冷缓存首帧会连发两个,直接拖慢状态栏。
13
- // 合并为一次,并在 Windows 上经 cmd `start /b` 蹦床发起(实测 ~17ms):node 的真实
14
- // 启动成本由蹦床在本进程退出后承担,不再占用状态栏进程的生命周期。
15
- // 任务负载写入 ~/.claude/ccglance/tasks/task-*.json,helper 只接收任务文件路径。
9
+ // stdout 写出后由 cli 调 flushRefreshTasks() 一次性 spawn 单个后台 helper(bg.js)。
10
+ // 合并为一次 spawn,避免冷缓存首帧连发两个后台进程;缓存都新鲜时 queue 为空、直接返回,
11
+ // 常规重绘不起任何后台进程。任务数据经 argv 直接传给 helper(不经 shell、无需临时文件)。
16
12
  const fs_1 = __importDefault(require("fs"));
17
13
  const path_1 = __importDefault(require("path"));
18
- const paths_1 = require("../utils/paths");
19
14
  const queue = [];
20
15
  // 入队前调用方必须已独占抢到对应 marker(多进程节流语义保持不变)。
21
16
  function queueRefreshTask(task) {
@@ -29,50 +24,29 @@ function removeMarkers(tasks) {
29
24
  catch { /* ignore */ }
30
25
  }
31
26
  }
32
- function writeTaskFile(tasks) {
33
- const dir = (0, paths_1.cacheDir)('tasks');
34
- fs_1.default.mkdirSync(dir, { recursive: true });
35
- const file = path_1.default.join(dir, `task-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.json`);
36
- const tmp = `${file}.tmp`;
37
- fs_1.default.writeFileSync(tmp, JSON.stringify(tasks));
38
- fs_1.default.renameSync(tmp, file);
39
- return file;
40
- }
41
- function quoteCmdArg(s) {
42
- return `"${s}"`;
43
- }
44
27
  // 渲染输出完成后调用:把积累的刷新任务交给一个 detached 后台 helper 进程。
45
28
  function flushRefreshTasks() {
46
29
  if (!queue.length)
47
30
  return;
48
31
  const tasks = queue.splice(0);
49
- let taskFile = '';
50
32
  try {
51
33
  const helper = path_1.default.join(__dirname, 'bg.js');
52
- taskFile = writeTaskFile(tasks);
34
+ const payload = JSON.stringify(tasks);
53
35
  const { spawn } = require('child_process');
54
- // Windows:node.exe 的 CreateProcess 同步成本很高(杀软扫描),改用 cmd `start /b`
55
- // 蹦床(发起 ~17ms),真正的 node 启动开销由蹦床在本进程退出后承担。
56
- // 任务本身在文件里;cmd 只需要转发 node/helper/taskFile 三个路径参数。
57
- const child = process.platform === 'win32'
58
- ? spawn('cmd.exe', ['/d', '/s', '/c', `start "" /b ${quoteCmdArg(process.execPath)} ${quoteCmdArg(helper)} ${quoteCmdArg(taskFile)}`], { detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: true })
59
- : spawn(process.execPath, [helper, taskFile], { detached: true, stdio: 'ignore' });
60
- child.on('error', () => {
61
- try {
62
- fs_1.default.rmSync(taskFile, { force: true });
63
- }
64
- catch { /* ignore */ }
65
- removeMarkers(tasks);
36
+ // 直接 detached spawn node,不经 shell:
37
+ // - 任务数据用一个 argv 参数(JSON)传给 helper;spawn 不走 shell,无引号/转义问题,
38
+ // 因此不需要临时任务文件(历史上用过 task-*.json,是残留主因,已弃用)。
39
+ // - detached 让 helper 脱离父进程独立跑完,并在结束时清理自己的 marker,避免残留。
40
+ // - Windows 关键坑:绝不同时设 detached + windowsHide —— 两个 flag 冲突会意外弹出
41
+ // 控制台黑窗。detached 单独即 DETACHED_PROCESS,本就不分配控制台,因此静默。
42
+ const child = spawn(process.execPath, [helper, payload], {
43
+ detached: true,
44
+ stdio: 'ignore',
66
45
  });
46
+ child.on('error', () => removeMarkers(tasks));
67
47
  child.unref();
68
48
  }
69
49
  catch {
70
- if (taskFile) {
71
- try {
72
- fs_1.default.rmSync(taskFile, { force: true });
73
- }
74
- catch { /* ignore */ }
75
- }
76
50
  removeMarkers(tasks);
77
51
  }
78
52
  }
@@ -9,7 +9,7 @@ const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const paths_1 = require("../utils/paths");
11
11
  const refresh_1 = require("./refresh");
12
- const CACHE_TTL_MS = 4 * 3600 * 1000;
12
+ const CACHE_TTL_MS = 2 * 3600 * 1000;
13
13
  const REFRESH_THROTTLE_MS = 60 * 1000;
14
14
  const cacheFile = path_1.default.join((0, paths_1.cacheDir)('version'), 'claude-code.json');
15
15
  const refreshFile = path_1.default.join((0, paths_1.cacheDir)('version'), 'claude-code.refresh');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cxmyu/ccglance",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "A fast, zero-dependency, multi-line status line for Claude Code — model, effort, context, cache, git and session, all at a glance.",
5
5
  "bin": {
6
6
  "ccglance": "dist/cli.js"