From 9f26a071a71d99990eb96d21fb28fd8a38bb5d81 Mon Sep 17 00:00:00 2001 From: Ramiro Rivera Date: Wed, 8 Oct 2025 05:36:13 +0200 Subject: [PATCH] FEAT: Support CLAUDE_CONFIG_DIR (#99) * chore: automatic change from linting * feat: support CLAUDE_CONFIG_DIR on utilities * feat: support CLAUDE_CONFIG_DIR from the main app UI * fix: reduce duplication and handle installation using new constants * feat: missed one place using hardcoded path * chore: update CLAUDE.md * docs: Update README --- CLAUDE.md | 4 ++ README.md | 20 +++++- src/tui/App.tsx | 94 +++++++++++++---------------- src/tui/components/InstallMenu.tsx | 6 +- src/tui/components/LineSelector.tsx | 14 ++--- src/utils/claude-settings.ts | 79 +++++++++++++++++++++--- 6 files changed, 144 insertions(+), 73 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6a26da4..b8db2dd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,10 @@ The project has dual runtime compatibility - works with both Bun and Node.js: - Manages flex separator expansion - **powerline.ts**: Powerline font detection and installation - **claude-settings.ts**: Integration with Claude Code settings.json + - Respects `CLAUDE_CONFIG_DIR` environment variable with fallback to `~/.claude` + - Provides installation command constants (NPM, BUNX, self-managed) + - Detects installation status and manages settings.json updates + - Validates config directory paths with proper error handling - **colors.ts**: Color definitions and ANSI code mapping ### Widgets (src/widgets/) diff --git a/README.md b/README.md index f5ef304..0b879fe 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ - **🖥️ Interactive TUI** - Built-in configuration interface using React/Ink - **⚙️ Global Options** - Apply consistent formatting across all widgets (padding, separators, bold, background) - **🚀 Cross-platform** - Works seamlessly with both Bun and Node.js +- **🔧 Flexible Configuration** - Supports custom Claude Code config directory via `CLAUDE_CONFIG_DIR` environment variable - **📏 Smart Width Detection** - Automatically adapts to terminal width with flex separators - **⚡ Zero Config** - Sensible defaults that work out of the box @@ -155,6 +156,15 @@ The interactive configuration tool provides a terminal UI where you can: > 💡 **Tip:** Your settings are automatically saved to `~/.config/ccstatusline/settings.json` +> 🔧 **Custom Claude Config:** If your Claude Code configuration is in a non-standard location, set the `CLAUDE_CONFIG_DIR` environment variable: +> ```bash +> # Linux/macOS +> export CLAUDE_CONFIG_DIR=/custom/path/to/.claude +> +> # Windows PowerShell +> $env:CLAUDE_CONFIG_DIR="C:\custom\path\.claude" +> ``` + --- ## 🪟 Windows Support @@ -294,7 +304,11 @@ For the best experience, configure Windows Terminal with these recommended setti #### Claude Code Integration Configure ccstatusline in your Claude Code settings: -**For Bun users** (Windows: `%USERPROFILE%\.claude\settings.json`): +**Settings Location:** +- Default: `~/.claude/settings.json` (Windows: `%USERPROFILE%\.claude\settings.json`) +- Custom: Set `CLAUDE_CONFIG_DIR` environment variable to use a different directory + +**For Bun users**: ```json { "statusLine": "bunx ccstatusline@latest" @@ -308,6 +322,8 @@ Configure ccstatusline in your Claude Code settings: } ``` +> 💡 **Custom Config Directory:** If you use a non-standard Claude Code configuration directory, set the `CLAUDE_CONFIG_DIR` environment variable before running ccstatusline. The tool will automatically detect and use your custom location. + ### Performance on Windows ccstatusline is optimized for Windows performance: @@ -561,7 +577,7 @@ ccstatusline/ │ │ ├── renderer.ts # Core rendering logic │ │ ├── powerline.ts # Powerline font utilities │ │ ├── colors.ts # Color definitions -│ │ └── claude-settings.ts # Claude Code integration +│ │ └── claude-settings.ts # Claude Code integration (supports CLAUDE_CONFIG_DIR) │ └── types/ # TypeScript type definitions │ ├── Settings.ts │ ├── Widget.ts diff --git a/src/tui/App.tsx b/src/tui/App.tsx index 19609f2..7cbef15 100644 --- a/src/tui/App.tsx +++ b/src/tui/App.tsx @@ -8,6 +8,7 @@ import { } from 'ink'; import Gradient from 'ink-gradient'; import React, { + useCallback, useEffect, useState } from 'react'; @@ -15,6 +16,8 @@ import React, { import type { Settings } from '../types/Settings'; import type { WidgetItem } from '../types/Widget'; import { + CCSTATUSLINE_COMMANDS, + getClaudeSettingsPath, getExistingStatusLine, installStatusLine, isBunxAvailable, @@ -129,6 +132,41 @@ export const App: React.FC = () => { } }); + const handleInstallSelection = useCallback((command: string, displayName: string, useBunx: boolean) => { + void getExistingStatusLine().then((existing) => { + const isAlreadyInstalled = [CCSTATUSLINE_COMMANDS.NPM, CCSTATUSLINE_COMMANDS.BUNX, CCSTATUSLINE_COMMANDS.SELF_MANAGED].includes(existing ?? ''); + let message: string; + + if (existing && !isAlreadyInstalled) { + message = `This will modify ${getClaudeSettingsPath()}\n\nA status line is already configured: "${existing}"\nReplace it with ${command}?`; + } else if (isAlreadyInstalled) { + message = `ccstatusline is already installed in ${getClaudeSettingsPath()}\nUpdate it with ${command}?`; + } else { + message = `This will modify ${getClaudeSettingsPath()} to add ccstatusline with ${displayName}.\nContinue?`; + } + + setConfirmDialog({ + message, + action: async () => { + await installStatusLine(useBunx); + setIsClaudeInstalled(true); + setExistingStatusLine(command); + setScreen('main'); + setConfirmDialog(null); + } + }); + setScreen('confirm'); + }); + }, []); + + const handleNpxInstall = useCallback(() => { + handleInstallSelection(CCSTATUSLINE_COMMANDS.NPM, 'npx', false); + }, [handleInstallSelection]); + + const handleBunxInstall = useCallback(() => { + handleInstallSelection(CCSTATUSLINE_COMMANDS.BUNX, 'bunx', true); + }, [handleInstallSelection]); + if (!settings) { return Loading settings...; } @@ -137,7 +175,7 @@ export const App: React.FC = () => { if (isClaudeInstalled) { // Uninstall setConfirmDialog({ - message: 'This will remove ccstatusline from ~/.claude/settings.json. Continue?', + message: `This will remove ccstatusline from ${getClaudeSettingsPath()}. Continue?`, action: async () => { await uninstallStatusLine(); setIsClaudeInstalled(false); @@ -376,58 +414,8 @@ export const App: React.FC = () => { { - void getExistingStatusLine().then((existing) => { - const isAlreadyInstalled = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'].includes(existing ?? ''); - let message: string; - - if (existing && !isAlreadyInstalled) { - message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with npx -y ccstatusline@latest?`; - } else if (isAlreadyInstalled) { - message = 'ccstatusline is already installed in ~/.claude/settings.json\nUpdate it with npx -y ccstatusline@latest?'; - } else { - message = 'This will modify ~/.claude/settings.json to add ccstatusline with npx.\nContinue?'; - } - - setConfirmDialog({ - message, - action: async () => { - await installStatusLine(false); - setIsClaudeInstalled(true); - setExistingStatusLine('npx -y ccstatusline@latest'); - setScreen('main'); - setConfirmDialog(null); - } - }); - setScreen('confirm'); - }); - }} - onSelectBunx={() => { - void getExistingStatusLine().then((existing) => { - const isAlreadyInstalled = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'].includes(existing ?? ''); - let message: string; - - if (existing && !isAlreadyInstalled) { - message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with bunx -y ccstatusline@latest?`; - } else if (isAlreadyInstalled) { - message = 'ccstatusline is already installed in ~/.claude/settings.json\nUpdate it with bunx -y ccstatusline@latest?'; - } else { - message = 'This will modify ~/.claude/settings.json to add ccstatusline with bunx.\nContinue?'; - } - - setConfirmDialog({ - message, - action: async () => { - await installStatusLine(true); - setIsClaudeInstalled(true); - setExistingStatusLine('bunx -y ccstatusline@latest'); - setScreen('main'); - setConfirmDialog(null); - } - }); - setScreen('confirm'); - }); - }} + onSelectNpx={handleNpxInstall} + onSelectBunx={handleBunxInstall} onCancel={() => { setScreen('main'); }} diff --git a/src/tui/components/InstallMenu.tsx b/src/tui/components/InstallMenu.tsx index 3a7cbf9..5160b26 100644 --- a/src/tui/components/InstallMenu.tsx +++ b/src/tui/components/InstallMenu.tsx @@ -5,6 +5,8 @@ import { } from 'ink'; import React, { useState } from 'react'; +import { getClaudeSettingsPath } from '../../utils/claude-settings'; + export interface InstallMenuProps { bunxAvailable: boolean; existingStatusLine: string | null; @@ -95,7 +97,9 @@ export const InstallMenu: React.FC = ({ - The selected command will be written to ~/.claude/settings.json + The selected command will be written to + {' '} + {getClaudeSettingsPath()} diff --git a/src/tui/components/LineSelector.tsx b/src/tui/components/LineSelector.tsx index da0e0e9..1733337 100644 --- a/src/tui/components/LineSelector.tsx +++ b/src/tui/components/LineSelector.tsx @@ -73,10 +73,10 @@ const LineSelector: React.FC = ({ const powerlineEnabled = settings ? settings.powerline.enabled : false; const powerlineTheme = settings ? settings.powerline.theme : undefined; const isThemeManaged - = blockIfPowerlineActive - && powerlineEnabled - && powerlineTheme - && powerlineTheme !== 'custom'; + = blockIfPowerlineActive + && powerlineEnabled + && powerlineTheme + && powerlineTheme !== 'custom'; // Handle keyboard input useInput((input, key) => { @@ -153,9 +153,9 @@ const LineSelector: React.FC = ({ if (showDeleteDialog && selectedLine) { const suffix - = selectedLine.length > 0 - ? pluralize('widget', selectedLine.length, true) - : 'empty'; + = selectedLine.length > 0 + ? pluralize('widget', selectedLine.length, true) + : 'empty'; return ( diff --git a/src/utils/claude-settings.ts b/src/utils/claude-settings.ts index 2fdc1a6..43c4620 100644 --- a/src/utils/claude-settings.ts +++ b/src/utils/claude-settings.ts @@ -13,32 +13,89 @@ const readFile = fs.promises.readFile; const writeFile = fs.promises.writeFile; const mkdir = fs.promises.mkdir; -const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json'); +export const CCSTATUSLINE_COMMANDS = { + NPM: 'npx -y ccstatusline@latest', + BUNX: 'bunx -y ccstatusline@latest', + SELF_MANAGED: 'ccstatusline' +}; + +/** + * Determines the Claude config directory, checking CLAUDE_CONFIG_DIR environment variable first, + * then falling back to the default ~/.claude directory. + */ +export function getClaudeConfigDir(): string { + const envConfigDir = process.env.CLAUDE_CONFIG_DIR; + + if (envConfigDir) { + try { + // Validate that the path is absolute and reasonable + const resolvedPath = path.resolve(envConfigDir); + + // Check if directory exists or can be created + if (fs.existsSync(resolvedPath)) { + const stats = fs.statSync(resolvedPath); + if (stats.isDirectory()) { + return resolvedPath; + } + } else { + // Directory doesn't exist yet, but we can try to use it + // (mkdir will be called later when saving) + return resolvedPath; + } + } catch { + // Fall through to default on any error + } + } + + // Default fallback + return path.join(os.homedir(), '.claude'); +} + +/** + * Gets the full path to the Claude settings.json file. + */ +export function getClaudeSettingsPath(): string { + return path.join(getClaudeConfigDir(), 'settings.json'); +} export async function loadClaudeSettings(): Promise { try { - if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) { + const settingsPath = getClaudeSettingsPath(); + if (!fs.existsSync(settingsPath)) { return {}; } - const content = await readFile(CLAUDE_SETTINGS_PATH, 'utf-8'); + const content = await readFile(settingsPath, 'utf-8'); return JSON.parse(content) as ClaudeSettings; } catch { return {}; } } -export async function saveClaudeSettings(settings: ClaudeSettings): Promise { - const dir = path.dirname(CLAUDE_SETTINGS_PATH); +export async function saveClaudeSettings( + settings: ClaudeSettings +): Promise { + const settingsPath = getClaudeSettingsPath(); + const dir = path.dirname(settingsPath); await mkdir(dir, { recursive: true }); - await writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8'); + await writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8'); } export async function isInstalled(): Promise { const settings = await loadClaudeSettings(); // Check if command is either npx or bunx version AND padding is 0 (or undefined for new installs) - const validCommands = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest']; - return validCommands.includes(settings.statusLine?.command ?? '') - && (settings.statusLine?.padding === 0 || settings.statusLine?.padding === undefined); + const validCommands = [ + // Default autoinstalled npm command + CCSTATUSLINE_COMMANDS.NPM, + // Default autoinstalled bunx command + CCSTATUSLINE_COMMANDS.BUNX, + // Self managed installation command + CCSTATUSLINE_COMMANDS.SELF_MANAGED + ]; + return ( + validCommands.includes(settings.statusLine?.command ?? '') + && (settings.statusLine?.padding === 0 + || settings.statusLine?.padding === undefined) + ); } export function isBunxAvailable(): boolean { @@ -58,7 +115,9 @@ export async function installStatusLine(useBunx = false): Promise { // Update settings with our status line (confirmation already handled in TUI) settings.statusLine = { type: 'command', - command: useBunx ? 'bunx -y ccstatusline@latest' : 'npx -y ccstatusline@latest', + command: useBunx + ? CCSTATUSLINE_COMMANDS.BUNX + : CCSTATUSLINE_COMMANDS.NPM, padding: 0 };