Skip to content

Commit 197bea1

Browse files
committed
feat(settings): Add support for local/project settings
1 parent 1d79ebe commit 197bea1

File tree

3 files changed

+160
-39
lines changed

3 files changed

+160
-39
lines changed

.claude/ccstatusline.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"version": 3,
3+
"lines": [
4+
[
5+
{
6+
"id": "1",
7+
"type": "model",
8+
"color": "cyan"
9+
},
10+
{
11+
"id": "2",
12+
"type": "separator"
13+
},
14+
{
15+
"id": "3",
16+
"type": "context-length",
17+
"color": "brightBlack"
18+
},
19+
{
20+
"id": "4",
21+
"type": "separator"
22+
},
23+
{
24+
"id": "5",
25+
"type": "git-branch",
26+
"color": "magenta"
27+
},
28+
{
29+
"id": "6",
30+
"type": "separator"
31+
},
32+
{
33+
"id": "7",
34+
"type": "git-changes",
35+
"color": "yellow"
36+
}
37+
]
38+
],
39+
"flexMode": "full-minus-40",
40+
"compactThreshold": 60,
41+
"colorLevel": 2,
42+
"inheritSeparatorColors": false,
43+
"globalBold": false,
44+
"powerline": {
45+
"enabled": false,
46+
"separators": [
47+
""
48+
],
49+
"separatorInvertBackground": [
50+
false
51+
],
52+
"startCaps": [],
53+
"endCaps": [],
54+
"autoAlign": false
55+
}
56+
}

src/tui/components/MainMenu.tsx

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import React, { useState } from 'react';
77

88
import type { Settings } from '../../types/Settings';
9+
import { getSettingsConfiguration } from '../../utils/config';
910
import { type PowerlineFontStatus } from '../../utils/powerline';
1011

1112
export interface MainMenuProps {
@@ -18,19 +19,37 @@ export interface MainMenuProps {
1819
previewIsTruncated?: boolean;
1920
}
2021

21-
export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasChanges, initialSelection = 0, powerlineFontStatus, settings, previewIsTruncated }) => {
22+
export const MainMenu: React.FC<MainMenuProps> = ({
23+
onSelect,
24+
isClaudeInstalled,
25+
hasChanges,
26+
initialSelection = 0,
27+
powerlineFontStatus,
28+
settings,
29+
previewIsTruncated
30+
}) => {
2231
const [selectedIndex, setSelectedIndex] = useState(initialSelection);
2332

2433
// Build menu structure with visual gaps
2534
const menuItems = [
2635
{ label: '📝 Edit Lines', value: 'lines', selectable: true },
2736
{ label: '🎨 Edit Colors', value: 'colors', selectable: true },
2837
{ label: '⚡ Powerline Setup', value: 'powerline', selectable: true },
29-
{ label: '', value: '_gap1', selectable: false }, // Visual gap
38+
{ label: '', value: '_gap1', selectable: false }, // Visual gap
3039
{ label: '💻 Terminal Options', value: 'terminalConfig', selectable: true },
31-
{ label: '🌐 Global Overrides', value: 'globalOverrides', selectable: true },
32-
{ label: '', value: '_gap2', selectable: false }, // Visual gap
33-
{ label: isClaudeInstalled ? '🔌 Uninstall from Claude Code' : '📦 Install to Claude Code', value: 'install', selectable: true }
40+
{
41+
label: '🌐 Global Overrides',
42+
value: 'globalOverrides',
43+
selectable: true
44+
},
45+
{ label: '', value: '_gap2', selectable: false }, // Visual gap
46+
{
47+
label: isClaudeInstalled
48+
? '🔌 Uninstall from Claude Code'
49+
: '📦 Install to Claude Code',
50+
value: 'install',
51+
selectable: true
52+
}
3453
];
3554

3655
if (hasChanges) {
@@ -61,14 +80,19 @@ export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled,
6180
// Get description for selected item
6281
const getDescription = (value: string): string => {
6382
const descriptions: Record<string, string> = {
64-
lines: 'Configure up to 3 status lines with various widgets like model info, git status, and token usage',
65-
colors: 'Customize colors for each widget including foreground, background, and bold styling',
66-
powerline: 'Install Powerline fonts for enhanced visual separators and symbols in your status line',
67-
globalOverrides: 'Set global padding, separators, and color overrides that apply to all widgets',
83+
lines:
84+
'Configure up to 3 status lines with various widgets like model info, git status, and token usage',
85+
colors:
86+
'Customize colors for each widget including foreground, background, and bold styling',
87+
powerline:
88+
'Install Powerline fonts for enhanced visual separators and symbols in your status line',
89+
globalOverrides:
90+
'Set global padding, separators, and color overrides that apply to all widgets',
6891
install: isClaudeInstalled
6992
? 'Remove ccstatusline from your Claude Code settings'
7093
: 'Add ccstatusline to your Claude Code settings for automatic status line rendering',
71-
terminalConfig: 'Configure terminal-specific settings for optimal display',
94+
terminalConfig:
95+
'Configure terminal-specific settings for optimal display',
7296
save: 'Save all changes and exit the configuration tool',
7397
exit: hasChanges
7498
? 'Exit without saving your changes'
@@ -81,16 +105,30 @@ export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled,
81105
const description = selectedItem ? getDescription(selectedItem.value) : '';
82106

83107
// Check if we should show the truncation warning
84-
const showTruncationWarning = previewIsTruncated && settings?.flexMode === 'full-minus-40';
108+
const showTruncationWarning
109+
= previewIsTruncated && settings?.flexMode === 'full-minus-40';
110+
111+
const { relativePath: settingsPath } = getSettingsConfiguration();
85112

86113
return (
87114
<Box flexDirection='column'>
88115
{showTruncationWarning && (
89116
<Box marginBottom={1}>
90-
<Text color='yellow'>⚠ Some lines are truncated, see Terminal Options → Terminal Width for info</Text>
117+
<Text color='yellow'>
118+
⚠ Some lines are truncated, see Terminal Options → Terminal Width
119+
for info
120+
</Text>
91121
</Box>
92122
)}
93-
<Text bold>Main Menu</Text>
123+
<Text>
124+
<Text bold>Main Menu</Text>
125+
<Text dimColor>
126+
{' '}
127+
(
128+
{settingsPath}
129+
)
130+
</Text>
131+
</Text>
94132
<Box marginTop={1} flexDirection='column'>
95133
{menuItems.map((item, idx) => {
96134
if (!item.selectable && item.value.startsWith('_gap')) {
@@ -100,10 +138,7 @@ export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled,
100138
const isSelected = selectableIdx === selectedIndex;
101139

102140
return (
103-
<Text
104-
key={item.value}
105-
color={isSelected ? 'green' : undefined}
106-
>
141+
<Text key={item.value} color={isSelected ? 'green' : undefined}>
107142
{isSelected ? '▶ ' : ' '}
108143
{item.label}
109144
</Text>
@@ -112,7 +147,9 @@ export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled,
112147
</Box>
113148
{description && (
114149
<Box marginTop={1} paddingLeft={2}>
115-
<Text dimColor wrap='wrap'>{description}</Text>
150+
<Text dimColor wrap='wrap'>
151+
{description}
152+
</Text>
116153
</Box>
117154
)}
118155
</Box>

src/utils/config.ts

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,39 @@ const readFile = fs.promises.readFile;
1919
const writeFile = fs.promises.writeFile;
2020
const mkdir = fs.promises.mkdir;
2121

22-
const CONFIG_DIR = path.join(os.homedir(), '.config', 'ccstatusline');
23-
const SETTINGS_PATH = process.env.CCSTATUSLINE_CONFIG ?? path.join(CONFIG_DIR, 'settings.json');
24-
const SETTINGS_BACKUP_PATH = path.join(CONFIG_DIR, 'settings.bak');
22+
export function getSettingsConfiguration() {
23+
const projectConfig = path.join(process.cwd(), '.claude', 'ccstatusline.json');
24+
25+
if (fs.existsSync(projectConfig)) {
26+
return {
27+
configDir: path.dirname(projectConfig),
28+
path: projectConfig,
29+
relativePath: path.relative(process.cwd(), projectConfig),
30+
type: 'project'
31+
};
32+
}
33+
34+
const userConfigDir = path.join(os.homedir(), '.config', 'ccstatusline');
35+
const userConfig = path.join(userConfigDir, 'settings.json');
36+
37+
// Fallback to global config
38+
return {
39+
configDir: userConfigDir,
40+
path: userConfig,
41+
relativePath: '~/' + path.relative(os.homedir(), userConfig),
42+
type: 'global'
43+
};
44+
}
2545

2646
async function backupBadSettings(): Promise<void> {
2747
try {
28-
if (fs.existsSync(SETTINGS_PATH)) {
29-
const content = await readFile(SETTINGS_PATH, 'utf-8');
30-
await writeFile(SETTINGS_BACKUP_PATH, content, 'utf-8');
31-
console.error(`Bad settings backed up to ${SETTINGS_BACKUP_PATH}`);
48+
const { path: settingsPath } = getSettingsConfiguration();
49+
const settingsBackupPath = settingsPath.replace('.json', '.json.bak');
50+
51+
if (fs.existsSync(settingsPath)) {
52+
const content = await readFile(settingsPath, 'utf-8');
53+
await writeFile(settingsBackupPath, content, 'utf-8');
54+
console.error(`Bad settings backed up to ${settingsBackupPath}`);
3255
}
3356
} catch (error) {
3457
console.error('Failed to backup bad settings:', error);
@@ -37,15 +60,10 @@ async function backupBadSettings(): Promise<void> {
3760

3861
async function writeDefaultSettings(): Promise<Settings> {
3962
const defaults = SettingsSchema.parse({});
40-
const settingsWithVersion = {
41-
...defaults,
42-
version: CURRENT_VERSION
43-
};
4463

4564
try {
46-
await mkdir(CONFIG_DIR, { recursive: true });
47-
await writeFile(SETTINGS_PATH, JSON.stringify(settingsWithVersion, null, 2), 'utf-8');
48-
console.error(`Default settings written to ${SETTINGS_PATH}`);
65+
const { path: settingsPath } = await saveSettings(defaults);
66+
console.error(`Default settings written to ${settingsPath}`);
4967
} catch (error) {
5068
console.error('Failed to write default settings:', error);
5169
}
@@ -55,11 +73,13 @@ async function writeDefaultSettings(): Promise<Settings> {
5573

5674
export async function loadSettings(): Promise<Settings> {
5775
try {
76+
const { path: settingsPath } = getSettingsConfiguration();
77+
5878
// Check if settings file exists
59-
if (!fs.existsSync(SETTINGS_PATH))
79+
if (!fs.existsSync(settingsPath))
6080
return await writeDefaultSettings();
6181

62-
const content = await readFile(SETTINGS_PATH, 'utf-8');
82+
const content = await readFile(settingsPath, 'utf-8');
6383
let rawData: unknown;
6484

6585
try {
@@ -84,16 +104,17 @@ export async function loadSettings(): Promise<Settings> {
84104

85105
// Migrate v1 to current version and save the migrated settings back to disk
86106
rawData = migrateConfig(rawData, CURRENT_VERSION);
87-
await writeFile(SETTINGS_PATH, JSON.stringify(rawData, null, 2), 'utf-8');
107+
await writeFile(settingsPath, JSON.stringify(rawData, null, 2), 'utf-8');
88108
} else if (needsMigration(rawData, CURRENT_VERSION)) {
89109
// Handle migrations for versioned configs (v2+) and save the migrated settings back to disk
90110
rawData = migrateConfig(rawData, CURRENT_VERSION);
91-
await writeFile(SETTINGS_PATH, JSON.stringify(rawData, null, 2), 'utf-8');
111+
await writeFile(settingsPath, JSON.stringify(rawData, null, 2), 'utf-8');
92112
}
93113

94114
// At this point, data should be in current format with version field
95115
// Parse with main schema which will apply all defaults
96116
const result = SettingsSchema.safeParse(rawData);
117+
97118
if (!result.success) {
98119
console.error('Failed to parse settings:', result.error);
99120
await backupBadSettings();
@@ -109,9 +130,11 @@ export async function loadSettings(): Promise<Settings> {
109130
}
110131
}
111132

112-
export async function saveSettings(settings: Settings): Promise<void> {
133+
export async function saveSettings(settings: Settings) {
134+
const { path, configDir } = getSettingsConfiguration();
135+
113136
// Ensure config directory exists
114-
await mkdir(CONFIG_DIR, { recursive: true });
137+
await mkdir(configDir, { recursive: true });
115138

116139
// Always include version when saving
117140
const settingsWithVersion = {
@@ -120,5 +143,10 @@ export async function saveSettings(settings: Settings): Promise<void> {
120143
};
121144

122145
// Write settings using Node.js-compatible API
123-
await writeFile(SETTINGS_PATH, JSON.stringify(settingsWithVersion, null, 2), 'utf-8');
146+
await writeFile(path, JSON.stringify(settingsWithVersion, null, 2), 'utf-8');
147+
148+
return {
149+
settings: settingsWithVersion,
150+
path
151+
};
124152
}

0 commit comments

Comments
 (0)