codebuddy-stats 1.0.0 → 1.1.0
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/.github/workflows/publish.yml +41 -0
- package/README.md +120 -0
- package/dist/index.js +76 -18
- package/dist/index.js.map +1 -1
- package/dist/lib/data-loader.js +289 -56
- package/dist/lib/data-loader.js.map +1 -1
- package/dist/lib/paths.js +31 -7
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/workspace-resolver.js +211 -0
- package/dist/lib/workspace-resolver.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +86 -20
- package/src/lib/data-loader.ts +346 -67
- package/src/lib/paths.ts +33 -8
- package/src/lib/workspace-resolver.ts +235 -0
- package/index.js +0 -16
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import fsSync from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import { getWorkspaceStorageDir } from './paths.js';
|
|
7
|
+
// 缓存已解析的 CodeBuddy Code 路径名
|
|
8
|
+
const codePathCache = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* 从 folder URI 提取纯路径(用于计算 MD5)
|
|
11
|
+
*/
|
|
12
|
+
function extractPathFromUri(folderUri) {
|
|
13
|
+
// 处理本地文件路径: file:///path/to/folder
|
|
14
|
+
if (folderUri.startsWith('file://')) {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(folderUri);
|
|
17
|
+
return decodeURIComponent(url.pathname);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return decodeURIComponent(folderUri.replace('file://', ''));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// 处理远程路径: vscode-remote://codebuddy-remote-ssh%2B.../path
|
|
24
|
+
if (folderUri.startsWith('vscode-remote://')) {
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(folderUri);
|
|
27
|
+
return decodeURIComponent(url.pathname);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
const match = folderUri.match(/vscode-remote:\/\/[^/]+(.+)$/);
|
|
31
|
+
if (match?.[1]) {
|
|
32
|
+
return decodeURIComponent(match[1]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 格式化路径用于显示(简化 home 目录)
|
|
40
|
+
*/
|
|
41
|
+
function formatDisplayPath(p) {
|
|
42
|
+
const home = os.homedir();
|
|
43
|
+
if (p.startsWith(home)) {
|
|
44
|
+
return '~' + p.slice(home.length);
|
|
45
|
+
}
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 从 folder URI 生成用于显示的友好路径
|
|
50
|
+
*/
|
|
51
|
+
function getDisplayPath(folderUri) {
|
|
52
|
+
// 本地路径
|
|
53
|
+
if (folderUri.startsWith('file://')) {
|
|
54
|
+
const p = extractPathFromUri(folderUri);
|
|
55
|
+
if (p) {
|
|
56
|
+
return formatDisplayPath(p);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// 远程路径
|
|
60
|
+
if (folderUri.startsWith('vscode-remote://')) {
|
|
61
|
+
const p = extractPathFromUri(folderUri);
|
|
62
|
+
if (p) {
|
|
63
|
+
const hostMatch = folderUri.match(/vscode-remote:\/\/codebuddy-remote-ssh%2B([^/]+)/);
|
|
64
|
+
if (hostMatch?.[1]) {
|
|
65
|
+
let host = decodeURIComponent(hostMatch[1]);
|
|
66
|
+
host = host.replace(/_x([0-9a-fA-F]{2})_/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
67
|
+
host = host.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
68
|
+
if (host.includes('@')) {
|
|
69
|
+
const parts = host.split('@');
|
|
70
|
+
host = parts[parts.length - 1]?.split(':')[0] || host;
|
|
71
|
+
}
|
|
72
|
+
if (host.length > 20) {
|
|
73
|
+
host = host.slice(0, 17) + '...';
|
|
74
|
+
}
|
|
75
|
+
return `[${host}]${p}`;
|
|
76
|
+
}
|
|
77
|
+
return `[remote]${p}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return folderUri;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 计算路径的 MD5 hash(CodeBuddyExtension 使用纯路径计算)
|
|
84
|
+
*/
|
|
85
|
+
function computePathHash(p) {
|
|
86
|
+
return crypto.createHash('md5').update(p).digest('hex');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 加载所有工作区映射
|
|
90
|
+
*/
|
|
91
|
+
export async function loadWorkspaceMappings() {
|
|
92
|
+
const mappings = new Map();
|
|
93
|
+
const storageDir = getWorkspaceStorageDir();
|
|
94
|
+
let entries = [];
|
|
95
|
+
try {
|
|
96
|
+
entries = await fs.readdir(storageDir, { withFileTypes: true });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return mappings;
|
|
100
|
+
}
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (!entry.isDirectory())
|
|
103
|
+
continue;
|
|
104
|
+
const workspaceJsonPath = path.join(storageDir, entry.name, 'workspace.json');
|
|
105
|
+
try {
|
|
106
|
+
const content = await fs.readFile(workspaceJsonPath, 'utf8');
|
|
107
|
+
const data = JSON.parse(content);
|
|
108
|
+
const folderUri = data.folder;
|
|
109
|
+
if (!folderUri)
|
|
110
|
+
continue;
|
|
111
|
+
const extractedPath = extractPathFromUri(folderUri);
|
|
112
|
+
if (!extractedPath)
|
|
113
|
+
continue;
|
|
114
|
+
const hash = computePathHash(extractedPath);
|
|
115
|
+
const displayPath = getDisplayPath(folderUri);
|
|
116
|
+
mappings.set(hash, { hash, folderUri, displayPath });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// 跳过无法读取的文件
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return mappings;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 检查路径是否存在(同步版本,用于路径探测)
|
|
126
|
+
*/
|
|
127
|
+
function pathExistsSync(p) {
|
|
128
|
+
try {
|
|
129
|
+
fsSync.accessSync(p);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 尝试将 CodeBuddy Code 的项目名(路径中 / 替换为 -)还原为真实路径
|
|
138
|
+
* 使用回溯搜索,因为目录名本身可能包含 -
|
|
139
|
+
*
|
|
140
|
+
* 例如: "Users-anoti-Documents-project-codebudy-cost-analyzer"
|
|
141
|
+
* -> "/Users/anoti/Documents/project/codebudy-cost-analyzer"
|
|
142
|
+
*/
|
|
143
|
+
function tryResolveCodePath(name) {
|
|
144
|
+
// 检查缓存
|
|
145
|
+
const cached = codePathCache.get(name);
|
|
146
|
+
if (cached !== undefined) {
|
|
147
|
+
return cached || null;
|
|
148
|
+
}
|
|
149
|
+
const parts = name.split('-');
|
|
150
|
+
if (parts.length < 2) {
|
|
151
|
+
codePathCache.set(name, '');
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
// 回溯搜索:尝试不同的分割方式
|
|
155
|
+
function backtrack(index, currentPath) {
|
|
156
|
+
if (index >= parts.length) {
|
|
157
|
+
// 检查完整路径是否存在
|
|
158
|
+
if (pathExistsSync(currentPath)) {
|
|
159
|
+
return currentPath;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
// 尝试从当前位置开始,合并不同数量的 parts
|
|
164
|
+
for (let end = index; end < parts.length; end++) {
|
|
165
|
+
const segment = parts.slice(index, end + 1).join('-');
|
|
166
|
+
const newPath = currentPath ? `${currentPath}/${segment}` : `/${segment}`;
|
|
167
|
+
// 如果这不是最后一段,检查目录是否存在
|
|
168
|
+
if (end < parts.length - 1) {
|
|
169
|
+
if (pathExistsSync(newPath)) {
|
|
170
|
+
const result = backtrack(end + 1, newPath);
|
|
171
|
+
if (result)
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// 最后一段,检查完整路径
|
|
177
|
+
if (pathExistsSync(newPath)) {
|
|
178
|
+
return newPath;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const result = backtrack(0, '');
|
|
185
|
+
codePathCache.set(name, result || '');
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 解析项目名称
|
|
190
|
+
* - MD5 hash (32位十六进制): 从 IDE workspaceMappings 查找
|
|
191
|
+
* - 路径格式 (包含 -): 尝试还原 CodeBuddy Code 的路径格式
|
|
192
|
+
*/
|
|
193
|
+
export function resolveProjectName(name, mappings) {
|
|
194
|
+
// IDE source: MD5 hash
|
|
195
|
+
if (mappings && /^[a-f0-9]{32}$/.test(name)) {
|
|
196
|
+
const mapping = mappings.get(name);
|
|
197
|
+
if (mapping) {
|
|
198
|
+
return mapping.displayPath;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Code source: 路径中 / 替换为 - 的格式
|
|
202
|
+
// 特征:以大写字母开头(如 Users-、home-),包含 -
|
|
203
|
+
if (/^[A-Za-z]/.test(name) && name.includes('-')) {
|
|
204
|
+
const resolved = tryResolveCodePath(name);
|
|
205
|
+
if (resolved) {
|
|
206
|
+
return formatDisplayPath(resolved);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return name;
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=workspace-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-resolver.js","sourceRoot":"","sources":["../../src/lib/workspace-resolver.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
3
6
|
import blessed from 'blessed'
|
|
4
7
|
|
|
5
8
|
import { loadUsageData } from './lib/data-loader.js'
|
|
6
9
|
import type { AnalysisData } from './lib/data-loader.js'
|
|
7
|
-
import {
|
|
10
|
+
import { resolveProjectName } from './lib/workspace-resolver.js'
|
|
8
11
|
import { formatCost, formatNumber, formatPercent, formatTokens, truncate } from './lib/utils.js'
|
|
9
12
|
|
|
13
|
+
// 读取 package.json 获取版本号
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
const pkgPath = path.resolve(__dirname, '../package.json')
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { version: string }
|
|
17
|
+
const VERSION = pkg.version
|
|
18
|
+
|
|
10
19
|
type CliOptions = {
|
|
11
20
|
days: number | null
|
|
12
21
|
noTui: boolean
|
|
@@ -73,7 +82,7 @@ function getHeatChar(cost: number, maxCost: number): string {
|
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
// 渲染 Overview 视图
|
|
76
|
-
function renderOverview(box: any, data: AnalysisData, width: number): void {
|
|
85
|
+
function renderOverview(box: any, data: AnalysisData, width: number, note: string): void {
|
|
77
86
|
const { dailySummary, grandTotal, topModel, topProject, cacheHitRate, activeDays } = data
|
|
78
87
|
const heatmap = generateHeatmapData(dailySummary)
|
|
79
88
|
|
|
@@ -159,15 +168,19 @@ function renderOverview(box: any, data: AnalysisData, width: number): void {
|
|
|
159
168
|
}
|
|
160
169
|
if (topProject) {
|
|
161
170
|
const projectMaxLen = width >= 100 ? 60 : 35
|
|
162
|
-
const shortName =
|
|
171
|
+
const shortName = resolveProjectName(topProject.name, data.workspaceMappings)
|
|
163
172
|
content += `{cyan-fg}Top project:{/cyan-fg} ${truncate(shortName, projectMaxLen)} (${formatCost(topProject.cost)})\n`
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
if (note) {
|
|
176
|
+
content += `\n{gray-fg}备注:${note}{/gray-fg}\n`
|
|
177
|
+
}
|
|
178
|
+
|
|
166
179
|
box.setContent(content)
|
|
167
180
|
}
|
|
168
181
|
|
|
169
182
|
// 渲染 By Model 视图
|
|
170
|
-
function renderByModel(box: any, data: AnalysisData, width: number): void {
|
|
183
|
+
function renderByModel(box: any, data: AnalysisData, width: number, note: string): void {
|
|
171
184
|
const { modelTotals, grandTotal } = data
|
|
172
185
|
const sorted = Object.entries(modelTotals).sort((a, b) => b[1].cost - a[1].cost)
|
|
173
186
|
|
|
@@ -207,11 +220,15 @@ function renderByModel(box: any, data: AnalysisData, width: number): void {
|
|
|
207
220
|
formatTokens(grandTotal.tokens).padStart(12) +
|
|
208
221
|
'{/bold}\n'
|
|
209
222
|
|
|
223
|
+
if (note) {
|
|
224
|
+
content += `\n{gray-fg}备注:${note}{/gray-fg}\n`
|
|
225
|
+
}
|
|
226
|
+
|
|
210
227
|
box.setContent(content)
|
|
211
228
|
}
|
|
212
229
|
|
|
213
230
|
// 渲染 By Project 视图
|
|
214
|
-
function renderByProject(box: any, data: AnalysisData, width: number): void {
|
|
231
|
+
function renderByProject(box: any, data: AnalysisData, width: number, note: string): void {
|
|
215
232
|
const { projectTotals, grandTotal } = data
|
|
216
233
|
const sorted = Object.entries(projectTotals).sort((a, b) => b[1].cost - a[1].cost)
|
|
217
234
|
|
|
@@ -232,7 +249,7 @@ function renderByProject(box: any, data: AnalysisData, width: number): void {
|
|
|
232
249
|
|
|
233
250
|
for (const [projectName, stats] of sorted) {
|
|
234
251
|
// 简化项目名
|
|
235
|
-
const shortName =
|
|
252
|
+
const shortName = resolveProjectName(projectName, data.workspaceMappings)
|
|
236
253
|
content +=
|
|
237
254
|
truncate(shortName, projectCol - 1).padEnd(projectCol) +
|
|
238
255
|
formatCost(stats.cost).padStart(12) +
|
|
@@ -250,11 +267,15 @@ function renderByProject(box: any, data: AnalysisData, width: number): void {
|
|
|
250
267
|
formatTokens(grandTotal.tokens).padStart(12) +
|
|
251
268
|
'{/bold}\n'
|
|
252
269
|
|
|
270
|
+
if (note) {
|
|
271
|
+
content += `\n{gray-fg}备注:${note}{/gray-fg}\n`
|
|
272
|
+
}
|
|
273
|
+
|
|
253
274
|
box.setContent(content)
|
|
254
275
|
}
|
|
255
276
|
|
|
256
277
|
// 渲染 Daily 视图
|
|
257
|
-
function renderDaily(box: any, data: AnalysisData, scrollOffset = 0, width: number): void {
|
|
278
|
+
function renderDaily(box: any, data: AnalysisData, scrollOffset = 0, width: number, note: string): void {
|
|
258
279
|
const { dailySummary, dailyData } = data
|
|
259
280
|
const sortedDates = Object.keys(dailySummary).sort().reverse()
|
|
260
281
|
|
|
@@ -303,7 +324,7 @@ function renderDaily(box: any, data: AnalysisData, scrollOffset = 0, width: numb
|
|
|
303
324
|
}
|
|
304
325
|
}
|
|
305
326
|
|
|
306
|
-
const shortProject =
|
|
327
|
+
const shortProject = resolveProjectName(topProject.name, data.workspaceMappings)
|
|
307
328
|
|
|
308
329
|
content +=
|
|
309
330
|
date.padEnd(dateCol) +
|
|
@@ -318,6 +339,10 @@ function renderDaily(box: any, data: AnalysisData, scrollOffset = 0, width: numb
|
|
|
318
339
|
content += `\n{gray-fg}Showing ${scrollOffset + 1}-${Math.min(scrollOffset + 20, sortedDates.length)} of ${sortedDates.length} days (↑↓ to scroll){/gray-fg}`
|
|
319
340
|
}
|
|
320
341
|
|
|
342
|
+
if (note) {
|
|
343
|
+
content += `\n\n{gray-fg}备注:${note}{/gray-fg}\n`
|
|
344
|
+
}
|
|
345
|
+
|
|
321
346
|
box.setContent(content)
|
|
322
347
|
}
|
|
323
348
|
|
|
@@ -338,7 +363,8 @@ function printTextReport(data: AnalysisData): void {
|
|
|
338
363
|
console.log(`\nTop model: ${topModel.id} (${formatCost(topModel.cost)})`)
|
|
339
364
|
}
|
|
340
365
|
if (topProject) {
|
|
341
|
-
|
|
366
|
+
const shortName = resolveProjectName(topProject.name, data.workspaceMappings)
|
|
367
|
+
console.log(`Top project: ${shortName}`)
|
|
342
368
|
console.log(` (${formatCost(topProject.cost)})`)
|
|
343
369
|
}
|
|
344
370
|
|
|
@@ -353,7 +379,7 @@ function printTextReport(data: AnalysisData): void {
|
|
|
353
379
|
for (const [project, stats] of Object.entries(projectTotals)
|
|
354
380
|
.sort((a, b) => b[1].cost - a[1].cost)
|
|
355
381
|
.slice(0, 10)) {
|
|
356
|
-
const shortName =
|
|
382
|
+
const shortName = resolveProjectName(project, data.workspaceMappings)
|
|
357
383
|
console.log(` ${truncate(shortName, 40)}: ${formatCost(stats.cost)}`) // eslint-disable-line no-console
|
|
358
384
|
}
|
|
359
385
|
|
|
@@ -365,7 +391,8 @@ async function main(): Promise<void> {
|
|
|
365
391
|
const options = parseArgs()
|
|
366
392
|
|
|
367
393
|
console.log('Loading data...')
|
|
368
|
-
let
|
|
394
|
+
let currentSource: 'code' | 'ide' = 'code'
|
|
395
|
+
let data = await loadUsageData({ days: options.days, source: currentSource })
|
|
369
396
|
|
|
370
397
|
if (options.noTui) {
|
|
371
398
|
printTextReport(data)
|
|
@@ -376,6 +403,8 @@ async function main(): Promise<void> {
|
|
|
376
403
|
const screen = blessed.screen({
|
|
377
404
|
smartCSR: true,
|
|
378
405
|
title: 'CodeBuddy Cost Analyzer',
|
|
406
|
+
forceUnicode: true,
|
|
407
|
+
fullUnicode: true,
|
|
379
408
|
})
|
|
380
409
|
|
|
381
410
|
// Tab 状态
|
|
@@ -438,6 +467,18 @@ async function main(): Promise<void> {
|
|
|
438
467
|
// 更新 Tab 栏
|
|
439
468
|
function updateTabBar(): void {
|
|
440
469
|
let content = ' Cost Analysis '
|
|
470
|
+
|
|
471
|
+
content += '{gray-fg}Source:{/gray-fg} '
|
|
472
|
+
if (currentSource === 'code') {
|
|
473
|
+
content += '{black-fg}{green-bg} Code {/green-bg}{/black-fg} '
|
|
474
|
+
content += '{gray-fg}IDE{/gray-fg} '
|
|
475
|
+
} else {
|
|
476
|
+
content += '{gray-fg}Code{/gray-fg} '
|
|
477
|
+
content += '{black-fg}{green-bg} IDE {/green-bg}{/black-fg} '
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
content += '{gray-fg}Views:{/gray-fg} '
|
|
481
|
+
|
|
441
482
|
for (let i = 0; i < tabs.length; i++) {
|
|
442
483
|
if (i === currentTab) {
|
|
443
484
|
content += `{black-fg}{green-bg} ${tabs[i]} {/green-bg}{/black-fg} `
|
|
@@ -445,25 +486,31 @@ async function main(): Promise<void> {
|
|
|
445
486
|
content += `{gray-fg}${tabs[i]}{/gray-fg} `
|
|
446
487
|
}
|
|
447
488
|
}
|
|
448
|
-
content += ' {gray-fg}(Tab
|
|
489
|
+
content += ' {gray-fg}(Tab view, s source){/gray-fg}'
|
|
449
490
|
tabBar.setContent(content)
|
|
450
491
|
}
|
|
451
492
|
|
|
452
493
|
// 更新内容
|
|
453
494
|
function updateContent(): void {
|
|
454
495
|
const width = Number(screen.width) || 80
|
|
496
|
+
|
|
497
|
+
const note =
|
|
498
|
+
currentSource === 'code'
|
|
499
|
+
? `针对 CodeBuddy Code ≤ 2.20.0 版本产生的数据,由于没有请求级别的 model ID,用量是基于当前 CodeBuddy Code 设置的 model ID(${data.defaultModelId})计算价格的`
|
|
500
|
+
: 'IDE 的 usage 不包含缓存命中/写入 tokens,无法计算缓存相关价格与命中率;成本按 input/output tokens 估算'
|
|
501
|
+
|
|
455
502
|
switch (currentTab) {
|
|
456
503
|
case 0:
|
|
457
|
-
renderOverview(contentBox, data, width)
|
|
504
|
+
renderOverview(contentBox, data, width, note)
|
|
458
505
|
break
|
|
459
506
|
case 1:
|
|
460
|
-
renderByModel(contentBox, data, width)
|
|
507
|
+
renderByModel(contentBox, data, width, note)
|
|
461
508
|
break
|
|
462
509
|
case 2:
|
|
463
|
-
renderByProject(contentBox, data, width)
|
|
510
|
+
renderByProject(contentBox, data, width, note)
|
|
464
511
|
break
|
|
465
512
|
case 3:
|
|
466
|
-
renderDaily(contentBox, data, dailyScrollOffset, width)
|
|
513
|
+
renderDaily(contentBox, data, dailyScrollOffset, width, note)
|
|
467
514
|
break
|
|
468
515
|
}
|
|
469
516
|
}
|
|
@@ -471,9 +518,12 @@ async function main(): Promise<void> {
|
|
|
471
518
|
// 更新状态栏
|
|
472
519
|
function updateStatusBar(): void {
|
|
473
520
|
const daysInfo = options.days ? `Last ${options.days} days` : 'All time'
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
521
|
+
const sourceInfo = currentSource === 'code' ? 'Code' : 'IDE'
|
|
522
|
+
const leftContent = ` ${daysInfo} | Source: ${sourceInfo} | Total: ${formatCost(data.grandTotal.cost)} | q quit, Tab view, s source, r refresh`
|
|
523
|
+
const rightContent = `v${VERSION} `
|
|
524
|
+
const width = Number(screen.width) || 80
|
|
525
|
+
const padding = Math.max(0, width - leftContent.length - rightContent.length)
|
|
526
|
+
statusBar.setContent(leftContent + ' '.repeat(padding) + rightContent)
|
|
477
527
|
}
|
|
478
528
|
|
|
479
529
|
// 键盘事件
|
|
@@ -519,7 +569,7 @@ async function main(): Promise<void> {
|
|
|
519
569
|
statusBar.setContent(' {yellow-fg}Reloading...{/yellow-fg}')
|
|
520
570
|
screen.render()
|
|
521
571
|
try {
|
|
522
|
-
data = await loadUsageData({ days: options.days })
|
|
572
|
+
data = await loadUsageData({ days: options.days, source: currentSource })
|
|
523
573
|
dailyScrollOffset = 0
|
|
524
574
|
updateTabBar()
|
|
525
575
|
updateContent()
|
|
@@ -530,6 +580,22 @@ async function main(): Promise<void> {
|
|
|
530
580
|
screen.render()
|
|
531
581
|
})
|
|
532
582
|
|
|
583
|
+
screen.key(['s'], async () => {
|
|
584
|
+
statusBar.setContent(' {yellow-fg}Switching source...{/yellow-fg}')
|
|
585
|
+
screen.render()
|
|
586
|
+
try {
|
|
587
|
+
currentSource = currentSource === 'code' ? 'ide' : 'code'
|
|
588
|
+
data = await loadUsageData({ days: options.days, source: currentSource })
|
|
589
|
+
dailyScrollOffset = 0
|
|
590
|
+
updateTabBar()
|
|
591
|
+
updateContent()
|
|
592
|
+
updateStatusBar()
|
|
593
|
+
} catch (err) {
|
|
594
|
+
statusBar.setContent(` {red-fg}Switch source failed: ${String(err)}{/red-fg}`)
|
|
595
|
+
}
|
|
596
|
+
screen.render()
|
|
597
|
+
})
|
|
598
|
+
|
|
533
599
|
// 监听窗口大小变化
|
|
534
600
|
screen.on('resize', () => {
|
|
535
601
|
updateContent()
|