Code cleanup and README

This commit is contained in:
Matthew Breedlove
2025-08-08 07:51:01 -04:00
parent d5c193c209
commit 07528510be
8 changed files with 1100 additions and 1055 deletions
+164
View File
@@ -0,0 +1,164 @@
# ccstatusline
A customizable status line formatter for Claude Code CLI that displays model info, git branch, token usage, and other metrics in your terminal.
## Features
- 📊 **Real-time metrics** - Display model name, git branch, token usage, and more
- 🎨 **Fully customizable** - Choose what to display and customize colors
- 🖥️ **Interactive TUI** - Built-in configuration interface using React/Ink
- 🚀 **Cross-platform** - Works with both Bun and Node.js
- 📏 **80-character format** - Perfectly sized for Claude Code CLI integration
## Quick Start
No installation needed! Use directly with npx:
```bash
# Run the configuration TUI
npx ccstatusline
```
## Setup
### Configure ccstatusline
Run the interactive configuration tool:
```bash
npx ccstatusline
```
This launches a TUI where you can:
- Add/remove status line items
- Reorder items with arrow keys
- Customize colors for each element
- Preview your status line in real-time
Your settings are saved to `~/.config/ccstatusline/settings.json`.
## Usage
Once configured, ccstatusline automatically formats your Claude Code status line. The status line appears at the bottom of your terminal during Claude Code sessions.
### Available Status Items
- **Model Name** - Shows the current Claude model (e.g., "Claude 3.5 Sonnet")
- **Git Branch** - Displays current git branch name
- **Token Usage** - Shows input/output/total tokens used
- **Time** - Current time in HH:MM:SS format
- **Custom Text** - Add your own static text
- **Separator** - Visual divider between items
- **Flex Separator** - Expands to fill available space
## Configuration File
The configuration file at `~/.config/ccstatusline/settings.json` looks like:
```json
{
"items": [
{
"type": "model",
"color": "cyan"
},
{
"type": "separator",
"text": " │ ",
"color": "gray"
},
{
"type": "git_branch",
"color": "green"
},
{
"type": "separator",
"text": " │ ",
"color": "gray"
},
{
"type": "tokens",
"color": "yellow"
},
{
"type": "flex_separator",
"text": "─",
"color": "gray"
},
{
"type": "time",
"color": "blue"
}
]
}
```
### Color Options
Available colors:
- `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`
- `brightBlack`, `brightRed`, `brightGreen`, `brightYellow`, `brightBlue`, `brightMagenta`, `brightCyan`, `brightWhite`
## Development
### Prerequisites
- [Bun](https://bun.sh)
- Git
### Setup
```bash
# Clone the repository
git clone https://github.com/yourusername/ccstatusline.git
cd ccstatusline
# Install dependencies
bun install
```
### Development Commands
```bash
# Run in TUI mode (configuration)
bun run src/ccstatusline.ts
# Build for distribution
bun run build
```
### Project Structure
```
ccstatusline/
├── src/
│ ├── ccstatusline.ts # Main entry point
│ ├── tui.tsx # React/Ink configuration UI
│ ├── config.ts # Settings management
│ └── claude-settings.ts # Claude Code settings integration
├── dist/ # Built files (generated)
├── package.json
├── tsconfig.json
└── README.md
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
MIT
## Author
Matthew Breedlove
## Acknowledgments
Built for use with [Claude Code CLI](https://claude.ai/code) by Anthropic.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ccstatusline",
"version": "1.0.0",
"version": "1.0.1",
"description": "A customizable status line formatter for Claude Code CLI",
"module": "src/ccstatusline.ts",
"type": "module",
+267 -263
View File
@@ -13,303 +13,307 @@ const readFile = fs.promises?.readFile || promisify(fs.readFile);
chalk.level = 3;
interface StatusJSON {
session_id: string;
transcript_path: string;
cwd: string;
model: {
id: string;
display_name: string;
};
workspace: {
current_dir: string;
project_dir: string;
};
session_id: string;
transcript_path: string;
cwd: string;
model: {
id: string;
display_name: string;
};
workspace: {
current_dir: string;
project_dir: string;
};
}
interface TokenUsage {
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
}
interface TranscriptLine {
message?: {
usage?: TokenUsage;
};
message?: {
usage?: TokenUsage;
};
}
async function readStdin(): Promise<string | null> {
// Check if stdin is a TTY (terminal) - if it is, there's no piped data
if (process.stdin.isTTY) {
return null;
}
const chunks: string[] = [];
try {
// Use Node.js compatible approach
if (typeof Bun !== 'undefined' && Bun.stdin) {
// Bun environment
const decoder = new TextDecoder();
for await (const chunk of Bun.stdin.stream()) {
chunks.push(decoder.decode(chunk));
}
} else {
// Node.js environment
process.stdin.setEncoding('utf8');
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
// Check if stdin is a TTY (terminal) - if it is, there's no piped data
if (process.stdin.isTTY) {
return null;
}
const chunks: string[] = [];
try {
// Use Node.js compatible approach
if (typeof Bun !== 'undefined' && Bun.stdin) {
// Bun environment
const decoder = new TextDecoder();
for await (const chunk of Bun.stdin.stream()) {
chunks.push(decoder.decode(chunk));
}
} else {
// Node.js environment
process.stdin.setEncoding('utf8');
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
}
return chunks.join('');
} catch {
return null;
}
return chunks.join('');
} catch {
return null;
}
}
function getGitBranch(): string | null {
try {
const branch = execSync('git branch --show-current 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
return branch || null;
} catch {
return null;
}
try {
const branch = execSync('git branch --show-current 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
return branch || null;
} catch {
return null;
}
}
async function getTokenMetrics(transcriptPath: string): Promise<{
inputTokens: number;
outputTokens: number;
cachedTokens: number;
totalTokens: number;
contextLength: number;
inputTokens: number;
outputTokens: number;
cachedTokens: number;
totalTokens: number;
contextLength: number;
}> {
try {
// Use Node.js-compatible file reading
if (!fs.existsSync(transcriptPath)) {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
const content = await readFile(transcriptPath, 'utf-8');
const lines = content.trim().split('\n');
let inputTokens = 0;
let outputTokens = 0;
let cachedTokens = 0;
// Parse each line and sum up token usage
for (const line of lines) {
try {
const data: TranscriptLine = JSON.parse(line);
if (data.message?.usage) {
inputTokens += data.message.usage.input_tokens || 0;
outputTokens += data.message.usage.output_tokens || 0;
cachedTokens += data.message.usage.cache_read_input_tokens || 0;
cachedTokens += data.message.usage.cache_creation_input_tokens || 0;
try {
// Use Node.js-compatible file reading
if (!fs.existsSync(transcriptPath)) {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
} catch {
// Skip invalid JSON lines
}
const content = await readFile(transcriptPath, 'utf-8');
const lines = content.trim().split('\n');
let inputTokens = 0;
let outputTokens = 0;
let cachedTokens = 0;
// Parse each line and sum up token usage
for (const line of lines) {
try {
const data: TranscriptLine = JSON.parse(line);
if (data.message?.usage) {
inputTokens += data.message.usage.input_tokens || 0;
outputTokens += data.message.usage.output_tokens || 0;
cachedTokens += data.message.usage.cache_read_input_tokens || 0;
cachedTokens += data.message.usage.cache_creation_input_tokens || 0;
}
} catch {
// Skip invalid JSON lines
}
}
const totalTokens = inputTokens + outputTokens + cachedTokens;
const contextLength = inputTokens + outputTokens; // Current context size
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
} catch {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
const totalTokens = inputTokens + outputTokens + cachedTokens;
const contextLength = inputTokens + outputTokens; // Current context size
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
} catch {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
}
async function renderStatusLine(data: StatusJSON) {
const settings = await loadSettings();
const terminalWidth = 80; // Always use 80 chars for Claude Code status line
const elements: { content: string, type: string }[] = [];
let hasFlexSeparator = false;
// Get token metrics if needed
const hasTokenItems = settings.items.some(item =>
['tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'].includes(item.type)
);
let tokenMetrics: any = null;
if (hasTokenItems && data.transcript_path) {
tokenMetrics = await getTokenMetrics(data.transcript_path);
}
// Helper function to format token counts
const formatTokens = (count: number): string => {
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
return count.toString();
};
// Build elements based on configured items
for (const item of settings.items) {
switch (item.type) {
case 'model':
if (data.model) {
const color = (chalk as any)[item.color || settings.colors.model] || chalk.cyan;
elements.push({ content: color(`Model: ${data.model.display_name}`), type: 'model' });
}
break;
case 'git-branch':
const branch = getGitBranch();
if (branch) {
const color = (chalk as any)[item.color || settings.colors.gitBranch] || chalk.magenta;
elements.push({ content: color(`${branch}`), type: 'git-branch' });
}
break;
case 'tokens-input':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
elements.push({ content: color(`In: ${formatTokens(tokenMetrics.inputTokens)}`), type: 'tokens-input' });
}
break;
case 'tokens-output':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'green'] || chalk.green;
elements.push({ content: color(`Out: ${formatTokens(tokenMetrics.outputTokens)}`), type: 'tokens-output' });
}
break;
case 'tokens-cached':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'blue'] || chalk.blue;
elements.push({ content: color(`Cached: ${formatTokens(tokenMetrics.cachedTokens)}`), type: 'tokens-cached' });
}
break;
case 'tokens-total':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'white'] || chalk.white;
elements.push({ content: color(`Total: ${formatTokens(tokenMetrics.totalTokens)}`), type: 'tokens-total' });
}
break;
case 'context-length':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
elements.push({ content: color(`Ctx: ${formatTokens(tokenMetrics.contextLength)}`), type: 'context-length' });
}
break;
case 'context-percentage':
if (tokenMetrics) {
const percentage = Math.min(100, (tokenMetrics.contextLength / 200000) * 100);
const color = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
elements.push({ content: color(`Ctx: ${percentage.toFixed(1)}%`), type: 'context-percentage' });
}
break;
case 'separator':
// Only add separator if there are already elements and the last one isn't a separator
if (elements.length > 0 && elements[elements.length - 1].type !== 'separator') {
const sepColor = (chalk as any)[settings.colors.separator] || chalk.dim;
elements.push({ content: sepColor(' | '), type: 'separator' });
}
break;
case 'flex-separator':
elements.push({ content: 'FLEX', type: 'flex-separator' });
hasFlexSeparator = true;
break;
}
}
if (elements.length === 0) return;
// Build the final status line
let statusLine = '';
if (hasFlexSeparator) {
// Split elements by flex separators
const parts: string[][] = [[]];
let currentPart = 0;
const settings = await loadSettings();
const terminalWidth = 80; // Always use 80 chars for Claude Code status line
const elements: { content: string, type: string }[] = [];
let hasFlexSeparator = false;
for (const elem of elements) {
if (elem.type === 'flex-separator') {
currentPart++;
parts[currentPart] = [];
} else {
parts[currentPart].push(elem.content);
}
// Get token metrics if needed
const hasTokenItems = settings.items.some(item =>
['tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'].includes(item.type)
);
let tokenMetrics: any = null;
if (hasTokenItems && data.transcript_path) {
tokenMetrics = await getTokenMetrics(data.transcript_path);
}
// Calculate total length of all non-flex content
const partLengths = parts.map(part => {
const joined = part.join('');
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
});
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
// Helper function to format token counts
const formatTokens = (count: number): string => {
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
return count.toString();
};
// Calculate space to distribute among flex separators
const flexCount = parts.length - 1; // Number of flex separators
const totalSpace = Math.max(0, terminalWidth - totalContentLength);
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
// Build the status line with distributed spacing
statusLine = '';
for (let i = 0; i < parts.length; i++) {
statusLine += parts[i].join('');
if (i < parts.length - 1) {
// Add flex spacing
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
statusLine += ' '.repeat(spaces);
}
// Build elements based on configured items
for (const item of settings.items) {
switch (item.type) {
case 'model':
if (data.model) {
const color = (chalk as any)[item.color || settings.colors.model] || chalk.cyan;
elements.push({ content: color(`Model: ${data.model.display_name}`), type: 'model' });
}
break;
case 'git-branch':
const branch = getGitBranch();
if (branch) {
const color = (chalk as any)[item.color || settings.colors.gitBranch] || chalk.magenta;
elements.push({ content: color(`${branch}`), type: 'git-branch' });
}
break;
case 'tokens-input':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
elements.push({ content: color(`In: ${formatTokens(tokenMetrics.inputTokens)}`), type: 'tokens-input' });
}
break;
case 'tokens-output':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'green'] || chalk.green;
elements.push({ content: color(`Out: ${formatTokens(tokenMetrics.outputTokens)}`), type: 'tokens-output' });
}
break;
case 'tokens-cached':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'blue'] || chalk.blue;
elements.push({ content: color(`Cached: ${formatTokens(tokenMetrics.cachedTokens)}`), type: 'tokens-cached' });
}
break;
case 'tokens-total':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'white'] || chalk.white;
elements.push({ content: color(`Total: ${formatTokens(tokenMetrics.totalTokens)}`), type: 'tokens-total' });
}
break;
case 'context-length':
if (tokenMetrics) {
const color = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
elements.push({ content: color(`Ctx: ${formatTokens(tokenMetrics.contextLength)}`), type: 'context-length' });
}
break;
case 'context-percentage':
if (tokenMetrics) {
const percentage = Math.min(100, (tokenMetrics.contextLength / 200000) * 100);
const color = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
elements.push({ content: color(`Ctx: ${percentage.toFixed(1)}%`), type: 'context-percentage' });
}
break;
case 'separator':
// Only add separator if there are already elements and the last one isn't a separator
const lastElement = elements[elements.length - 1];
if (elements.length > 0 && lastElement && lastElement.type !== 'separator') {
const sepColor = (chalk as any)[settings.colors.separator] || chalk.dim;
elements.push({ content: sepColor(' | '), type: 'separator' });
}
break;
case 'flex-separator':
elements.push({ content: 'FLEX', type: 'flex-separator' });
hasFlexSeparator = true;
break;
}
}
} else {
// No flex separator, just join all elements
statusLine = elements.map(e => e.content).join('');
// Pad to full width with spaces
const contentLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
const remainingSpace = terminalWidth - contentLength;
if (remainingSpace > 0) {
statusLine = statusLine + ' '.repeat(remainingSpace);
if (elements.length === 0) return;
// Build the final status line
let statusLine = '';
if (hasFlexSeparator) {
// Split elements by flex separators
const parts: string[][] = [[]];
let currentPart = 0;
for (const elem of elements) {
if (elem.type === 'flex-separator') {
currentPart++;
parts[currentPart] = [];
} else {
parts[currentPart]!.push(elem.content);
}
}
// Calculate total length of all non-flex content
const partLengths = parts.map(part => {
const joined = part.join('');
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
});
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
// Calculate space to distribute among flex separators
const flexCount = parts.length - 1; // Number of flex separators
const totalSpace = Math.max(0, terminalWidth - totalContentLength);
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
// Build the status line with distributed spacing
statusLine = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part) {
statusLine += part.join('');
}
if (i < parts.length - 1) {
// Add flex spacing
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
statusLine += ' '.repeat(spaces);
}
}
} else {
// No flex separator, just join all elements
statusLine = elements.map(e => e.content).join('');
// Pad to full width with spaces
const contentLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
const remainingSpace = terminalWidth - contentLength;
if (remainingSpace > 0) {
statusLine = statusLine + ' '.repeat(remainingSpace);
}
}
// Ensure we never exceed 80 chars
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > 80) {
// Truncate with ellipsis if too long
const visibleText = statusLine.replace(/\x1b\[[0-9;]*m/g, '');
const truncated = visibleText.substring(0, 77) + '...';
console.log(truncated);
} else {
console.log(statusLine);
}
}
// Ensure we never exceed 80 chars
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > 80) {
// Truncate with ellipsis if too long
const visibleText = statusLine.replace(/\x1b\[[0-9;]*m/g, '');
const truncated = visibleText.substring(0, 77) + '...';
console.log(truncated);
} else {
console.log(statusLine);
}
}
async function main() {
// Check if we're in a piped/non-TTY environment first
if (!process.stdin.isTTY) {
// We're receiving piped input
const input = await readStdin();
if (input && input.trim() !== '') {
try {
const data: StatusJSON = JSON.parse(input);
await renderStatusLine(data);
} catch (error) {
console.error('Error parsing JSON:', error);
process.exit(1);
}
// Check if we're in a piped/non-TTY environment first
if (!process.stdin.isTTY) {
// We're receiving piped input
const input = await readStdin();
if (input && input.trim() !== '') {
try {
const data: StatusJSON = JSON.parse(input);
await renderStatusLine(data);
} catch (error) {
console.error('Error parsing JSON:', error);
process.exit(1);
}
} else {
console.error('No input received');
process.exit(1);
}
} else {
console.error('No input received');
process.exit(1);
// Interactive mode - run TUI
runTUI();
}
} else {
// Interactive mode - run TUI
await runTUI();
}
}
main();
+41 -41
View File
@@ -11,64 +11,64 @@ const mkdir = fs.promises?.mkdir || promisify(fs.mkdir);
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
interface ClaudeSettings {
permissions?: {
allow?: string[];
deny?: string[];
};
statusLine?: {
type: string;
command: string;
padding?: number;
};
[key: string]: any;
permissions?: {
allow?: string[];
deny?: string[];
};
statusLine?: {
type: string;
command: string;
padding?: number;
};
[key: string]: any;
}
export async function loadClaudeSettings(): Promise<ClaudeSettings> {
try {
if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) {
return {};
try {
if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) {
return {};
}
const content = await readFile(CLAUDE_SETTINGS_PATH, 'utf-8');
return JSON.parse(content);
} catch {
return {};
}
const content = await readFile(CLAUDE_SETTINGS_PATH, 'utf-8');
return JSON.parse(content);
} catch {
return {};
}
}
export async function saveClaudeSettings(settings: ClaudeSettings): Promise<void> {
const dir = path.dirname(CLAUDE_SETTINGS_PATH);
await mkdir(dir, { recursive: true });
await writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
const dir = path.dirname(CLAUDE_SETTINGS_PATH);
await mkdir(dir, { recursive: true });
await writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
}
export async function isInstalled(): Promise<boolean> {
const settings = await loadClaudeSettings();
return settings.statusLine?.command === 'npx ccstatusline';
const settings = await loadClaudeSettings();
return settings.statusLine?.command === 'npx ccstatusline';
}
export async function installStatusLine(): Promise<void> {
const settings = await loadClaudeSettings();
// Update settings with our status line (confirmation already handled in TUI)
settings.statusLine = {
type: 'command',
command: 'npx ccstatusline',
padding: 1
};
await saveClaudeSettings(settings);
const settings = await loadClaudeSettings();
// Update settings with our status line (confirmation already handled in TUI)
settings.statusLine = {
type: 'command',
command: 'npx ccstatusline',
padding: 1
};
await saveClaudeSettings(settings);
}
export async function uninstallStatusLine(): Promise<void> {
const settings = await loadClaudeSettings();
if (settings.statusLine) {
delete settings.statusLine;
await saveClaudeSettings(settings);
}
const settings = await loadClaudeSettings();
if (settings.statusLine) {
delete settings.statusLine;
await saveClaudeSettings(settings);
}
}
export async function getExistingStatusLine(): Promise<string | null> {
const settings = await loadClaudeSettings();
return settings.statusLine?.command || null;
const settings = await loadClaudeSettings();
return settings.statusLine?.command || null;
}
+70 -70
View File
@@ -9,95 +9,95 @@ const writeFile = fs.promises?.writeFile || promisify(fs.writeFile);
const mkdir = fs.promises?.mkdir || promisify(fs.mkdir);
export type StatusItemType = 'model' | 'git-branch' | 'separator' | 'flex-separator' |
'tokens-input' | 'tokens-output' | 'tokens-cached' | 'tokens-total' | 'context-length' | 'context-percentage';
'tokens-input' | 'tokens-output' | 'tokens-cached' | 'tokens-total' | 'context-length' | 'context-percentage';
export interface StatusItem {
id: string;
type: StatusItemType;
color?: string;
id: string;
type: StatusItemType;
color?: string;
}
export interface Settings {
items: StatusItem[];
colors: {
model: string;
gitBranch: string;
separator: string;
};
items: StatusItem[];
colors: {
model: string;
gitBranch: string;
separator: string;
};
}
const CONFIG_DIR = path.join(os.homedir(), '.config', 'ccstatusline');
const SETTINGS_PATH = path.join(CONFIG_DIR, 'settings.json');
export const DEFAULT_SETTINGS: Settings = {
items: [
{ id: '1', type: 'model', color: 'cyan' },
{ id: '2', type: 'separator' },
{ id: '3', type: 'git-branch', color: 'magenta' },
],
colors: {
model: 'cyan',
gitBranch: 'magenta',
separator: 'dim',
},
items: [
{ id: '1', type: 'model', color: 'cyan' },
{ id: '2', type: 'separator' },
{ id: '3', type: 'git-branch', color: 'magenta' },
],
colors: {
model: 'cyan',
gitBranch: 'magenta',
separator: 'dim',
},
};
export async function loadSettings(): Promise<Settings> {
try {
// Use Node.js-compatible file reading
if (!fs.existsSync(SETTINGS_PATH)) {
return DEFAULT_SETTINGS;
try {
// Use Node.js-compatible file reading
if (!fs.existsSync(SETTINGS_PATH)) {
return DEFAULT_SETTINGS;
}
const content = await readFile(SETTINGS_PATH, 'utf-8');
const loaded = JSON.parse(content);
// Migrate old format if needed
if (loaded.elements || loaded.layout) {
return migrateOldSettings(loaded);
}
return { ...DEFAULT_SETTINGS, ...loaded };
} catch {
return DEFAULT_SETTINGS;
}
const content = await readFile(SETTINGS_PATH, 'utf-8');
const loaded = JSON.parse(content);
// Migrate old format if needed
if (loaded.elements || loaded.layout) {
return migrateOldSettings(loaded);
}
return { ...DEFAULT_SETTINGS, ...loaded };
} catch {
return DEFAULT_SETTINGS;
}
}
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 => {
if (item.type === 'separator') {
item.type = 'flex-separator';
}
});
}
return {
items,
colors: old.colors || DEFAULT_SETTINGS.colors,
};
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 => {
if (item.type === 'separator') {
item.type = 'flex-separator';
}
});
}
return {
items,
colors: old.colors || DEFAULT_SETTINGS.colors,
};
}
export async function saveSettings(settings: Settings): Promise<void> {
// 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');
// 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');
}
-1
View File
@@ -1 +0,0 @@
console.log("Hello via Bun!");
-148
View File
@@ -1,148 +0,0 @@
import * as p from '@clack/prompts';
import chalk from 'chalk';
import { loadSettings, saveSettings, type Settings } from './config';
const COLOR_OPTIONS = [
{ value: 'black', label: chalk.black('black') },
{ value: 'red', label: chalk.red('red') },
{ value: 'green', label: chalk.green('green') },
{ value: 'yellow', label: chalk.yellow('yellow') },
{ value: 'blue', label: chalk.blue('blue') },
{ value: 'magenta', label: chalk.magenta('magenta') },
{ value: 'cyan', label: chalk.cyan('cyan') },
{ value: 'white', label: chalk.white('white') },
{ value: 'gray', label: chalk.gray('gray') },
{ value: 'redBright', label: chalk.redBright('redBright') },
{ value: 'greenBright', label: chalk.greenBright('greenBright') },
{ value: 'yellowBright', label: chalk.yellowBright('yellowBright') },
{ value: 'blueBright', label: chalk.blueBright('blueBright') },
{ value: 'magentaBright', label: chalk.magentaBright('magentaBright') },
{ value: 'cyanBright', label: chalk.cyanBright('cyanBright') },
{ value: 'whiteBright', label: chalk.whiteBright('whiteBright') },
];
function previewStatusLine(settings: Settings): string {
const elements: string[] = [];
if (settings.elements.model) {
const modelColor = (chalk as any)[settings.colors.model] || chalk.white;
elements.push(modelColor('Model: Claude'));
}
if (settings.elements.gitBranch) {
const branchColor = (chalk as any)[settings.colors.gitBranch] || chalk.white;
elements.push(branchColor('Branch: main'));
}
const separatorColor = (chalk as any)[settings.colors.separator] || chalk.dim;
return elements.join(separatorColor(' | '));
}
export async function runTUI() {
let settings = await loadSettings();
console.clear();
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
let continueLoop = true;
while (continueLoop) {
console.log('\n' + chalk.dim('Current preview:'));
console.log(' ' + previewStatusLine(settings) + '\n');
const action = await p.select({
message: 'What would you like to configure?',
options: [
{ value: 'elements', label: '🔧 Toggle Elements' },
{ value: 'colors', label: '🎨 Configure Colors' },
{ value: 'save', label: '💾 Save & Exit' },
{ value: 'exit', label: '❌ Exit without saving' },
],
});
if (p.isCancel(action)) {
continueLoop = false;
break;
}
switch (action) {
case 'elements':
console.clear();
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
console.log('\n' + chalk.dim('Current preview:'));
console.log(' ' + previewStatusLine(settings) + '\n');
const selectedElements = await p.multiselect({
message: 'Select which elements to display:',
options: [
{
value: 'model',
label: 'Model',
hint: settings.elements.model ? 'currently enabled' : 'currently disabled'
},
{
value: 'gitBranch',
label: 'Git Branch',
hint: settings.elements.gitBranch ? 'currently enabled' : 'currently disabled'
},
],
initialValues: Object.entries(settings.elements)
.filter(([_, enabled]) => enabled)
.map(([key, _]) => key),
required: false,
});
if (!p.isCancel(selectedElements)) {
settings.elements.model = selectedElements.includes('model');
settings.elements.gitBranch = selectedElements.includes('gitBranch');
}
console.clear();
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
break;
case 'colors':
console.clear();
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
console.log('\n' + chalk.dim('Current preview:'));
console.log(' ' + previewStatusLine(settings) + '\n');
const colorElement = await p.select({
message: 'Which element color to configure?',
options: [
{ value: 'model', label: 'Model' },
{ value: 'gitBranch', label: 'Git Branch' },
{ value: 'separator', label: 'Separator' },
{ value: 'back', label: '← Back' },
],
});
if (!p.isCancel(colorElement) && colorElement !== 'back') {
console.clear();
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
console.log('\n' + chalk.dim('Current preview:'));
console.log(' ' + previewStatusLine(settings) + '\n');
const color = await p.select({
message: `Select color for ${colorElement}:`,
options: COLOR_OPTIONS,
initialValue: settings.colors[colorElement as keyof typeof settings.colors],
});
if (!p.isCancel(color)) {
(settings.colors as any)[colorElement] = color;
}
}
console.clear();
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
break;
case 'save':
await saveSettings(settings);
p.outro(chalk.green('✅ Settings saved successfully!'));
continueLoop = false;
break;
case 'exit':
p.outro(chalk.yellow('Exited without saving'));
continueLoop = false;
break;
}
}
}
+557 -531
View File
File diff suppressed because it is too large Load Diff