diff --git a/package.json b/package.json index b9094a2..a99db2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccstatusline", - "version": "1.0.11", + "version": "1.0.12", "description": "A customizable status line formatter for Claude Code CLI", "module": "src/ccstatusline.ts", "type": "module", diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index c18036c..8723eab 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -79,7 +79,7 @@ function getTerminalWidth(): number | null { stdio: ['pipe', 'pipe', 'ignore'], shell: '/bin/sh' }).trim(); - + // Check if we got a valid tty (not ?? which means no tty) if (tty && tty !== '??' && tty !== '?') { // Now get the terminal size @@ -91,7 +91,7 @@ function getTerminalWidth(): number | null { shell: '/bin/sh' } ).trim(); - + const parsed = parseInt(width, 10); if (!isNaN(parsed) && parsed > 0) { return parsed; @@ -100,14 +100,14 @@ function getTerminalWidth(): number | null { } catch { // Command failed, width detection not available } - + // Fallback: try tput cols which might work in some environments try { const width = execSync('tput cols 2>/dev/null', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); - + const parsed = parseInt(width, 10); if (!isNaN(parsed) && parsed > 0) { return parsed; @@ -115,7 +115,7 @@ function getTerminalWidth(): number | null { } catch { // tput also failed } - + return null; } @@ -179,7 +179,7 @@ async function getSessionDuration(transcriptPath: string): Promise line.trim()); - + if (lines.length === 0) { return null; } @@ -219,10 +219,10 @@ async function getSessionDuration(transcriptPath: string): Promise= threshold) { // Context is high, leave space for auto-compact terminalWidth = detectedWidth - 40; @@ -544,7 +544,7 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON, if (detectedWidth && detectedWidth > 0) { // Remove ANSI escape codes to get actual length const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length; - + if (plainLength > detectedWidth) { // Need to truncate - preserve ANSI codes while truncating let truncated = ''; @@ -552,10 +552,10 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON, let inAnsiCode = false; let ansiBuffer = ''; const targetLength = detectedWidth - 7; // Truncate to width-7 for proper fit - + for (let i = 0; i < statusLine.length; i++) { const char = statusLine[i]; - + if (char === '\x1b') { inAnsiCode = true; ansiBuffer = char; @@ -575,17 +575,17 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON, } } } - + statusLine = truncated + '...'; } } - + return statusLine; } async function renderStatusLine(data: StatusJSON) { const settings = await loadSettings(); - + // Get all lines to render (support both old items format and new lines format) let lines: StatusItem[][] = []; if (settings.lines) { @@ -596,29 +596,29 @@ async function renderStatusLine(data: StatusJSON) { } else { lines = [[]]; } - + // Get token metrics if needed (check all lines) - const hasTokenItems = lines.some(line => + const hasTokenItems = lines.some(line => line.some(item => ['tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'].includes(item.type) ) ); - + // Check if session clock is needed const hasSessionClock = lines.some(line => line.some(item => item.type === 'session-clock') ); - + let tokenMetrics: any = null; if (hasTokenItems && data.transcript_path) { tokenMetrics = await getTokenMetrics(data.transcript_path); } - + let sessionDuration: string | null = null; if (hasSessionClock && data.transcript_path) { sessionDuration = await getSessionDuration(data.transcript_path); } - + // Render each line for (const lineItems of lines) { if (lineItems.length > 0) { diff --git a/src/config.ts b/src/config.ts index 49e1532..45b5f5c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,7 +8,7 @@ const readFile = fs.promises?.readFile || promisify(fs.readFile); const writeFile = fs.promises?.writeFile || promisify(fs.writeFile); const mkdir = fs.promises?.mkdir || promisify(fs.mkdir); -export type StatusItemType = 'model' | 'git-branch' | 'git-changes' | 'separator' | 'flex-separator' | +export type StatusItemType = 'model' | 'git-branch' | 'git-changes' | 'separator' | 'flex-separator' | 'tokens-input' | 'tokens-output' | 'tokens-cached' | 'tokens-total' | 'context-length' | 'context-percentage' | 'terminal-width' | 'session-clock' | 'version' | 'custom-text'; export interface StatusItem { @@ -40,13 +40,38 @@ const SETTINGS_PATH = path.join(CONFIG_DIR, 'settings.json'); export const DEFAULT_SETTINGS: Settings = { lines: [ [ - { id: '1', type: 'model', color: 'cyan' }, - { id: '2', type: 'separator' }, - { id: '3', type: 'terminal-width', color: 'dim' }, - { id: '4', type: 'separator' }, - { id: '5', type: 'git-branch', color: 'magenta' }, - { id: '6', type: 'separator' }, - { id: '7', type: 'git-changes', color: 'yellow' }, + { + "id": "1", + "type": "model", + "color": "cyan" + }, + { + "id": "2", + "type": "separator" + }, + { + "id": "3", + "type": "context-length", + "color": "dim" + }, + { + "id": "4", + "type": "separator" + }, + { + "id": "5", + "type": "git-branch", + "color": "magenta" + }, + { + "id": "6", + "type": "separator" + }, + { + "id": "7", + "type": "git-changes", + "color": "yellow" + } ] ], colors: { @@ -55,7 +80,7 @@ export const DEFAULT_SETTINGS: Settings = { separator: 'dim', }, flexMode: 'full-minus-40', - compactThreshold: 75, + compactThreshold: 60, }; export async function loadSettings(): Promise { @@ -64,10 +89,10 @@ export async function loadSettings(): Promise { if (!fs.existsSync(SETTINGS_PATH)) { return DEFAULT_SETTINGS; } - + const content = await readFile(SETTINGS_PATH, 'utf-8'); let loaded: any; - + try { loaded = JSON.parse(content); } catch (parseError) { @@ -75,18 +100,18 @@ export async function loadSettings(): Promise { console.error('Failed to parse settings.json, using defaults'); return DEFAULT_SETTINGS; } - + // Migrate from old format with elements/layout if (loaded.elements || loaded.layout) { return migrateOldSettings(loaded); } - + // Migrate from single items array to lines array if (loaded.items && !loaded.lines) { loaded.lines = [loaded.items]; delete loaded.items; } - + // Ensure lines is an array and limit to 3 lines if (loaded.lines) { if (!Array.isArray(loaded.lines)) { @@ -94,7 +119,7 @@ export async function loadSettings(): Promise { } loaded.lines = loaded.lines.slice(0, 3); } - + return { ...DEFAULT_SETTINGS, ...loaded }; } catch (error) { // Any other error, return defaults @@ -106,19 +131,19 @@ export async function loadSettings(): Promise { function migrateOldSettings(old: any): Settings { const items: StatusItem[] = []; let id = 1; - + if (old.elements?.model) { items.push({ id: String(id++), type: 'model', color: old.colors?.model }); } - + if (items.length > 0 && old.elements?.gitBranch) { items.push({ id: String(id++), type: 'separator' }); } - + if (old.elements?.gitBranch) { items.push({ id: String(id++), type: 'git-branch', color: old.colors?.gitBranch }); } - + if (old.layout?.expandingSeparators) { // Replace regular separators with flex separators items.forEach(item => { @@ -127,7 +152,7 @@ function migrateOldSettings(old: any): Settings { } }); } - + return { lines: [items], // Put migrated items in first line colors: old.colors || DEFAULT_SETTINGS.colors, @@ -139,7 +164,7 @@ function migrateOldSettings(old: any): Settings { export async function saveSettings(settings: Settings): Promise { // Ensure config directory exists await mkdir(CONFIG_DIR, { recursive: true }); - + // Write settings using Node.js-compatible API await writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8'); } \ No newline at end of file diff --git a/src/tui.tsx b/src/tui.tsx index 3ad8a07..11c41fd 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -220,7 +220,7 @@ const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetec if (width && width > 0) { // Remove ANSI escape codes to get actual length const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length; - + if (plainLength > width) { // Need to truncate - preserve ANSI codes while truncating let truncated = ''; @@ -228,10 +228,10 @@ const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetec let inAnsiCode = false; let ansiBuffer = ''; const targetLength = width - 5; // Reserve 3 chars for ellipsis + 2 for proper fit in preview - + for (let i = 0; i < statusLine.length; i++) { const char = statusLine[i]; - + if (char === '\x1b') { inAnsiCode = true; ansiBuffer = char; @@ -251,7 +251,7 @@ const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetec } } } - + statusLine = truncated + '...'; } } @@ -783,9 +783,9 @@ interface FlexOptionsProps { const FlexOptions: React.FC = ({ settings, onUpdate, onBack }) => { const [selectedOption, setSelectedOption] = useState(settings.flexMode || 'full-minus-40'); - const [compactThreshold, setCompactThreshold] = useState(settings.compactThreshold || 75); + const [compactThreshold, setCompactThreshold] = useState(settings.compactThreshold || 60); const [editingThreshold, setEditingThreshold] = useState(false); - const [thresholdInput, setThresholdInput] = useState(String(settings.compactThreshold || 75)); + const [thresholdInput, setThresholdInput] = useState(String(settings.compactThreshold || 60)); const [validationError, setValidationError] = useState(null); const [highlightedOption, setHighlightedOption] = useState(settings.flexMode || 'full-minus-40');