UNPKG

ccusage-widget

Version:

A beautiful macOS desktop widget that displays your Claude Code usage statistics in real-time

433 lines 16.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const electron_1 = require("electron"); const path = __importStar(require("path")); const child_process_1 = require("child_process"); const util_1 = require("util"); const package_json_1 = __importDefault(require("../package.json")); const execAsync = (0, util_1.promisify)(child_process_1.exec); // Set app name before app is ready (important for macOS) // Use productName from package.json if available, otherwise use name const appName = package_json_1.default.build?.productName || package_json_1.default.name || 'CCUsage Widget'; electron_1.app.setName(appName); // Helper function to extract session name from sessionId function extractSessionNameFromId(sessionId) { if (!sessionId) return 'session'; // Match pattern: -Users-${whoami}-workspace-${sessionType}-${sessionName} // Extract the session name (the part after the second dash after workspace) const match = sessionId.match(/-workspace-[^-]+-([^-]+)/); if (match && match[1]) { return match[1]; } // If that doesn't work, try to get anything after the last dash const parts = sessionId.split('-'); if (parts.length > 0) { const lastPart = parts[parts.length - 1]; if (lastPart && lastPart !== 'workspace') { return lastPart; } } // Fallback to 'session' if pattern doesn't match return 'session'; } let mainWindow = null; let tray = null; let isQuitting = false; const isDev = process.argv.includes('--dev'); const config = { alwaysOnTop: true, opacity: 0.95, refreshInterval: 60000, // 1 minute position: 'top-right' }; function createWindow() { const { width: screenWidth, height: screenHeight } = electron_1.screen.getPrimaryDisplay().workAreaSize; const windowWidth = 400; const windowHeight = 300; const minHeight = 200; const maxHeight = 600; let x = 0, y = 0; switch (config.position) { case 'top-right': x = screenWidth - windowWidth - 20; y = 20; break; case 'top-left': x = 20; y = 20; break; case 'bottom-right': x = screenWidth - windowWidth - 20; y = screenHeight - windowHeight - 20; break; case 'bottom-left': x = 20; y = screenHeight - windowHeight - 20; break; } mainWindow = new electron_1.BrowserWindow({ width: windowWidth, height: windowHeight, minWidth: windowWidth, maxWidth: windowWidth, minHeight: minHeight, maxHeight: maxHeight, x, y, frame: false, transparent: true, alwaysOnTop: config.alwaysOnTop, resizable: true, movable: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false }, titleBarStyle: 'customButtonsOnHover', vibrancy: 'under-window', visualEffectState: 'active' }); mainWindow.setOpacity(config.opacity); const indexPath = isDev ? path.join(__dirname, '..', 'src', 'index.html') : path.join(__dirname, 'index.html'); mainWindow.loadFile(indexPath); if (isDev) { mainWindow.webContents.openDevTools({ mode: 'detach' }); } mainWindow.on('close', (event) => { if (!isQuitting) { event.preventDefault(); mainWindow?.hide(); } }); mainWindow.on('closed', () => { mainWindow = null; }); } function createTray() { const iconPath = path.join(__dirname, '..', 'assets', 'icon-template.png'); const icon = electron_1.nativeImage.createFromPath(iconPath); tray = new electron_1.Tray(icon.resize({ width: 16, height: 16 })); const contextMenu = electron_1.Menu.buildFromTemplate([ { label: 'Show Widget', click: () => { mainWindow?.show(); } }, { label: 'Hide Widget', click: () => { mainWindow?.hide(); } }, { type: 'separator' }, { label: 'Position', submenu: [ { label: 'Top Right', type: 'radio', checked: config.position === 'top-right', click: () => updatePosition('top-right') }, { label: 'Top Left', type: 'radio', checked: config.position === 'top-left', click: () => updatePosition('top-left') }, { label: 'Bottom Right', type: 'radio', checked: config.position === 'bottom-right', click: () => updatePosition('bottom-right') }, { label: 'Bottom Left', type: 'radio', checked: config.position === 'bottom-left', click: () => updatePosition('bottom-left') } ] }, { label: 'Always on Top', type: 'checkbox', checked: config.alwaysOnTop, click: (menuItem) => { config.alwaysOnTop = menuItem.checked; mainWindow?.setAlwaysOnTop(config.alwaysOnTop); } }, { type: 'separator' }, { label: 'Refresh Now', click: () => { mainWindow?.webContents.send('refresh-data'); } }, { type: 'separator' }, { label: 'Quit', click: () => { isQuitting = true; electron_1.app.quit(); } } ]); tray.setToolTip('CCUsage Widget'); tray.setContextMenu(contextMenu); tray.on('click', () => { if (mainWindow?.isVisible()) { mainWindow.hide(); } else { mainWindow?.show(); } }); } function updatePosition(position) { config.position = position; if (mainWindow) { const { width: screenWidth, height: screenHeight } = electron_1.screen.getPrimaryDisplay().workAreaSize; const [width, height] = mainWindow.getSize(); let x = 0, y = 0; switch (position) { case 'top-right': x = screenWidth - width - 20; y = 20; break; case 'top-left': x = 20; y = 20; break; case 'bottom-right': x = screenWidth - width - 20; y = screenHeight - height - 20; break; case 'bottom-left': x = 20; y = screenHeight - height - 20; break; } mainWindow.setPosition(x, y); } } electron_1.app.whenReady().then(() => { // Set About panel options for macOS electron_1.app.setAboutPanelOptions({ applicationName: appName, applicationVersion: package_json_1.default.version, version: package_json_1.default.version, copyright: 'Copyright © 2025 JeongJaeSoon', authors: package_json_1.default.author ? [typeof package_json_1.default.author === 'string' ? package_json_1.default.author : package_json_1.default.author.name || 'JeongJaeSoon'] : ['JeongJaeSoon'], website: package_json_1.default.homepage || 'https://github.com/JeongJaeSoon/ccusage-widget' }); createWindow(); createTray(); electron_1.app.on('activate', () => { if (electron_1.BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); electron_1.app.on('window-all-closed', () => { if (process.platform !== 'darwin') { electron_1.app.quit(); } }); electron_1.app.on('before-quit', () => { isQuitting = true; }); // IPC handlers for fetching usage data electron_1.ipcMain.handle('get-usage-data', async () => { try { // Execute ccusage commands to get data // Try different ways to execute ccusage let dailyResult, monthlyResult, sessionResult, blocksResult; try { // First try with npx [dailyResult, monthlyResult, sessionResult, blocksResult] = await Promise.all([ execAsync('npx ccusage daily --json'), execAsync('npx ccusage monthly --json'), execAsync('npx ccusage session --json'), execAsync('npx ccusage blocks --json') ]); } catch (error) { // Try with direct ccusage command // Try with direct ccusage command try { [dailyResult, monthlyResult, sessionResult, blocksResult] = await Promise.all([ execAsync('ccusage daily --json'), execAsync('ccusage monthly --json'), execAsync('ccusage session --json'), execAsync('ccusage blocks --json') ]); } catch (error2) { // Try with local node_modules // Try with local node_modules const ccusagePath = path.join(__dirname, '..', 'node_modules', '.bin', 'ccusage'); [dailyResult, monthlyResult, sessionResult, blocksResult] = await Promise.all([ execAsync(`${ccusagePath} daily --json`), execAsync(`${ccusagePath} monthly --json`), execAsync(`${ccusagePath} session --json`), execAsync(`${ccusagePath} blocks --json`) ]); } } let dailyData, monthlyData, sessionData, blocksData; try { const dailyParsed = JSON.parse(dailyResult.stdout || '{}'); const monthlyParsed = JSON.parse(monthlyResult.stdout || '{}'); const sessionParsed = JSON.parse(sessionResult.stdout || '{}'); const blocksParsed = JSON.parse(blocksResult.stdout || '{}'); // Adjust to actual ccusage output structure dailyData = dailyParsed; monthlyData = monthlyParsed; sessionData = sessionParsed; blocksData = blocksParsed; } catch (parseError) { console.error('Failed to parse JSON:', parseError); console.error('Raw outputs:', { daily: dailyResult?.stdout, monthly: monthlyResult?.stdout, session: sessionResult?.stdout, blocks: blocksResult?.stdout }); throw new Error('Failed to parse ccusage output'); } // Get today's usage (find today's date in the array) // Use local date to match ccusage output const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const todayDate = `${year}-${month}-${day}`; const dailyArray = dailyData.daily || dailyData.data || []; // Find today's data or get the most recent day let today = dailyArray.find((d) => d.date === todayDate); // If today's data is not found, get the most recent (last item in array) if (!today && dailyArray.length > 0) { today = dailyArray[dailyArray.length - 1]; } // Get this month's usage const thisMonthDate = new Date().toISOString().substring(0, 7); // YYYY-MM format const monthlyArray = monthlyData.monthly || monthlyData.data || []; const thisMonth = monthlyArray.find((m) => m.month === thisMonthDate) || monthlyArray[0] || null; // Get total from summary const totalDaily = dailyData.totals || dailyData.summary || {}; // Get recent sessions const sessionsArray = sessionData.sessions || sessionData.data || []; const recentSessions = sessionsArray.slice(0, 5).map((s) => { // Extract session name from sessionId if available, otherwise use fallback names let sessionName = s.sessionName || s.session || s.name || 'Unknown Session'; // If we have a sessionId, try to extract the session name from it if (s.sessionId) { const extractedName = extractSessionNameFromId(s.sessionId); sessionName = extractedName; } return { name: sessionName, tokens: s.totalTokens || 0, cost: s.totalCost || s.costUSD || s.totalCostUSD || 0, lastActivity: s.lastActivity || s.lastUpdated || new Date().toISOString() }; }); // Get blocks data const blocksArray = Array.isArray(blocksData.blocks) ? blocksData.blocks : []; // Find current active block const currentBlock = blocksArray.find((block) => block && typeof block === 'object' && block.isActive === true); const result = { today: today ? { date: today.date, tokens: today.totalTokens, cost: today.totalCost || today.costUSD || today.totalCostUSD, models: today.modelsUsed || today.models || [] } : null, thisMonth: thisMonth ? { tokens: thisMonth.totalTokens, cost: thisMonth.totalCost || thisMonth.costUSD || thisMonth.totalCostUSD } : null, total: { tokens: totalDaily.totalTokens || 0, cost: totalDaily.totalCost || totalDaily.totalCostUSD || 0 }, currentBlock: currentBlock ? { tokens: (currentBlock.tokenCounts?.inputTokens || 0) + (currentBlock.tokenCounts?.outputTokens || 0), cost: currentBlock.costUSD || 0, startTime: currentBlock.startTime, endTime: currentBlock.endTime, isActive: true } : null, recentSessions, lastUpdated: new Date().toISOString() }; return result; } catch (error) { console.error('Error loading usage data:', error); // Return empty data instead of throwing return { today: null, thisMonth: null, total: { tokens: 0, cost: 0 }, currentBlock: null, recentSessions: [], lastUpdated: new Date().toISOString(), error: error instanceof Error ? error.message : 'Unknown error' }; } }); electron_1.ipcMain.handle('update-opacity', async (_, opacity) => { config.opacity = opacity; mainWindow?.setOpacity(opacity); }); //# sourceMappingURL=main.js.map