mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-09-02 10:19:34 +00:00
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
This commit is contained in:
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
|
||||
+41
-53
@@ -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 <Text>Loading settings...</Text>;
|
||||
}
|
||||
@@ -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 = () => {
|
||||
<InstallMenu
|
||||
bunxAvailable={isBunxAvailable()}
|
||||
existingStatusLine={existingStatusLine}
|
||||
onSelectNpx={() => {
|
||||
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');
|
||||
}}
|
||||
|
||||
@@ -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<InstallMenuProps> = ({
|
||||
|
||||
<Box marginTop={2}>
|
||||
<Text dimColor>
|
||||
The selected command will be written to ~/.claude/settings.json
|
||||
The selected command will be written to
|
||||
{' '}
|
||||
{getClaudeSettingsPath()}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -73,10 +73,10 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
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<LineSelectorProps> = ({
|
||||
|
||||
if (showDeleteDialog && selectedLine) {
|
||||
const suffix
|
||||
= selectedLine.length > 0
|
||||
? pluralize('widget', selectedLine.length, true)
|
||||
: 'empty';
|
||||
= selectedLine.length > 0
|
||||
? pluralize('widget', selectedLine.length, true)
|
||||
: 'empty';
|
||||
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
|
||||
@@ -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<ClaudeSettings> {
|
||||
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<void> {
|
||||
const dir = path.dirname(CLAUDE_SETTINGS_PATH);
|
||||
export async function saveClaudeSettings(
|
||||
settings: ClaudeSettings
|
||||
): Promise<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
// 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
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user