Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Normalize line endings to LF for all text files
* text=auto eol=lf

# Binary files
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.ttf binary
*.otf binary
*.woff binary
*.woff2 binary
18 changes: 16 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ bun run build # Creates dist/ccstatusline.js with Node.js 14+ compatibility

# Lint and type check
bun run lint # Runs TypeScript type checking and ESLint with auto-fix

# Run specific components during development
bun run patch # Apply patches (required before development)
```

## Architecture
Expand Down Expand Up @@ -55,20 +58,31 @@ The project has dual runtime compatibility - works with both Bun and Node.js:
- Handles terminal width detection and truncation
- Applies colors, padding, and separators
- Manages flex separator expansion
- Supports both normal and Powerline rendering modes
- **powerline.ts**: Powerline font detection and installation
- **claude-settings.ts**: Integration with Claude Code settings.json
- **colors.ts**: Color definitions and ANSI code mapping
- **widgets.ts**: Widget registry and factory functions
- **terminal.ts**: Terminal width detection utilities
- **jsonl.ts**: JSONL parsing for token metrics and session data

### Widgets (src/widgets/)
- Individual widget implementations for status line items
- Each widget exports render() and getDefaultColor() functions
- Includes built-in widgets: Model, GitBranch, TokensTotal, OutputStyle, BlockTimer, etc.
- Custom widgets: CustomText and CustomCommand for user-defined content

## Key Implementation Details

- **Cross-platform stdin reading**: Detects Bun vs Node.js environment and uses appropriate stdin API
- **Dual runtime support**: Detects Bun vs Node.js environment and uses appropriate stdin API
- **Token metrics**: Parses Claude Code transcript files (JSONL format) to calculate token usage
- **Git integration**: Uses child_process.execSync to get current branch and changes
- **Terminal width management**: Three modes for handling width (full, full-minus-40, full-until-compact)
- **Flex separators**: Special separator type that expands to fill available space
- **Powerline mode**: Optional Powerline-style rendering with arrow separators
- **Powerline mode**: Optional Powerline-style rendering with arrow separators and theme support
- **Custom commands**: Execute shell commands and display output in status line
- **Mergeable items**: Items can be merged together with or without padding
- **Widget registry**: Factory pattern for widget creation and type safety

## Bun Usage Preferences

Expand Down
67 changes: 65 additions & 2 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"start": "bun run patch && bun run src/ccstatusline.ts",
"statusline": "bun run src/ccstatusline.ts",
"patch": "patch-package",
"build": "bun run patch && rm -rf dist/* && bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --target-version=14",
"build": "bun run patch && rimraf dist && bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --target-version=14",
"postbuild": "bun run scripts/replace-version.ts",
"prepublishOnly": "bun run build",
"lint": "bun tsc --noEmit; eslint . --config eslint.config.js --max-warnings=999999 --fix"
Expand All @@ -35,8 +35,8 @@
"ink-gradient": "^3.0.0",
"ink-select-input": "^6.2.0",
"patch-package": "^8.0.0",
"rimraf": "^5.0.5",
"react": "^19.1.1",
"react-devtools-core": "^6.1.5",
"tinyglobby": "^0.2.14",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.1"
Expand Down
5 changes: 1 addition & 4 deletions src/ccstatusline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,14 @@ async function main() {
// Parse and validate JSON in one step
const result = StatusJSONSchema.safeParse(JSON.parse(input));
if (!result.success) {
console.error('Invalid status JSON format:', result.error.message);
process.exit(1);
}

await renderMultipleLines(result.data);
} catch (error) {
console.error('Error parsing JSON:', error);
} catch {
process.exit(1);
}
} else {
console.error('No input received');
process.exit(1);
}
} else {
Expand Down
7 changes: 1 addition & 6 deletions src/tui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
saveSettings
} from '../utils/config';
import {
checkPowerlineFonts,
checkPowerlineFontsAsync,
installPowerlineFonts,
type PowerlineFontStatus
Expand Down Expand Up @@ -81,11 +80,7 @@ export const App: React.FC = () => {
});
void isInstalled().then(setIsClaudeInstalled);

// Check for Powerline fonts on startup (use sync version that doesn't call execSync)
const fontStatus = checkPowerlineFonts();
setPowerlineFontStatus(fontStatus);

// Optionally do the async check later (but not blocking React)
// Check for Powerline fonts on startup
void checkPowerlineFontsAsync().then((asyncStatus) => {
setPowerlineFontStatus(asyncStatus);
});
Expand Down
4 changes: 3 additions & 1 deletion src/utils/claude-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ export async function isInstalled(): Promise<boolean> {

export function isBunxAvailable(): boolean {
try {
execSync('which bunx', { stdio: 'ignore' });
// The `where` command is the equivalent of `which` on Windows.
const command = os.platform() === 'win32' ? 'where' : 'which';
execSync(`${command} bunx`, { stdio: 'ignore' });
return true;
} catch {
return false;
Expand Down
16 changes: 5 additions & 11 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ async function backupBadSettings(): Promise<void> {
if (fs.existsSync(SETTINGS_PATH)) {
const content = await readFile(SETTINGS_PATH, 'utf-8');
await writeFile(SETTINGS_BACKUP_PATH, content, 'utf-8');
console.error(`Bad settings backed up to ${SETTINGS_BACKUP_PATH}`);
}
} catch (error) {
console.error('Failed to backup bad settings:', error);
} catch {
// Ignore backup errors
}
}

Expand All @@ -45,9 +44,8 @@ async function writeDefaultSettings(): Promise<Settings> {
try {
await mkdir(CONFIG_DIR, { recursive: true });
await writeFile(SETTINGS_PATH, JSON.stringify(settingsWithVersion, null, 2), 'utf-8');
console.error(`Default settings written to ${SETTINGS_PATH}`);
} catch (error) {
console.error('Failed to write default settings:', error);
} catch {
// Ignore write errors
}

return defaults;
Expand All @@ -66,7 +64,6 @@ export async function loadSettings(): Promise<Settings> {
rawData = JSON.parse(content);
} catch {
// If we can't parse the JSON, backup and write defaults
console.error('Failed to parse settings.json, backing up and using defaults');
await backupBadSettings();
return await writeDefaultSettings();
}
Expand All @@ -77,7 +74,6 @@ export async function loadSettings(): Promise<Settings> {
// Parse as v1 to validate before migration
const v1Result = SettingsSchema_v1.safeParse(rawData);
if (!v1Result.success) {
console.error('Invalid v1 settings format:', v1Result.error);
await backupBadSettings();
return await writeDefaultSettings();
}
Expand All @@ -95,15 +91,13 @@ export async function loadSettings(): Promise<Settings> {
// Parse with main schema which will apply all defaults
const result = SettingsSchema.safeParse(rawData);
if (!result.success) {
console.error('Failed to parse settings:', result.error);
await backupBadSettings();
return await writeDefaultSettings();
}

return result.data;
} catch (error) {
} catch {
// Any other error, backup and write defaults
console.error('Error loading settings:', error);
await backupBadSettings();
return await writeDefaultSettings();
}
Expand Down
24 changes: 0 additions & 24 deletions src/utils/powerline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,10 @@ import type { PowerlineFontStatus } from '../types/PowerlineFontStatus';
// Re-export for backward compatibility
export type { PowerlineFontStatus };

// Track if fonts were installed during this session (for DEBUG_FONT_INSTALL)
let fontsInstalledThisSession = false;

/**
* Check if Powerline fonts are installed by testing if Powerline symbols render correctly
*/
export function checkPowerlineFonts(): PowerlineFontStatus {
// Debug mode: pretend fonts aren't installed (unless we installed them this session)
if (process.env.DEBUG_FONT_INSTALL === '1' && !fontsInstalledThisSession) {
return {
installed: false,
checkedSymbol: '\uE0B0'
};
}

try {
// Test if we can display the common Powerline separator symbols
// These are the key characters that require Powerline fonts
Expand Down Expand Up @@ -108,14 +97,6 @@ export async function checkPowerlineFontsAsync(): Promise<PowerlineFontStatus> {
// Ensure this is always async
await Promise.resolve();

// Debug mode: pretend fonts aren't installed (unless we installed them this session)
if (process.env.DEBUG_FONT_INSTALL === '1' && !fontsInstalledThisSession) {
return {
installed: false,
checkedSymbol: '\uE0B0'
};
}

try {
// First do the quick synchronous check
const quickCheck = checkPowerlineFonts();
Expand Down Expand Up @@ -232,11 +213,6 @@ export async function installPowerlineFonts(): Promise<{ success: boolean; messa
}
}

// Mark as installed for DEBUG_FONT_INSTALL mode
if (process.env.DEBUG_FONT_INSTALL === '1') {
fontsInstalledThisSession = true;
}

return {
success: true,
message: 'Powerline fonts installed successfully! Please restart your terminal and select a Powerline font (e.g., "Source Code Pro for Powerline", "Meslo LG S for Powerline", etc.)'
Expand Down
87 changes: 4 additions & 83 deletions src/utils/terminal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';

Expand Down Expand Up @@ -34,93 +33,15 @@ export function getPackageVersion(): string {

// Get terminal width
export function getTerminalWidth(): number | null {
try {
// First try to get the tty of the parent process
const tty = execSync('ps -o tty= -p $(ps -o ppid= -p $$)', {
encoding: 'utf8',
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
const width = execSync(
`stty size < /dev/${tty} | awk '{print $2}'`,
{
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}
).trim();

const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
}
} 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;
}
} catch {
// tput also failed
const width = process.stdout.columns;
if (width) {
return width;
}

return null;
}

// Check if terminal width detection is available
export function canDetectTerminalWidth(): boolean {
try {
// First try to get the tty of the parent process
const tty = execSync('ps -o tty= -p $(ps -o ppid= -p $$)', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}).trim();

// Check if we got a valid tty
if (tty && tty !== '??' && tty !== '?') {
const width = execSync(
`stty size < /dev/${tty} | awk '{print $2}'`,
{
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}
).trim();

const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return true;
}
}
} catch {
// Try fallback
}

// Fallback: try tput cols
try {
const width = execSync('tput cols 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();

const parsed = parseInt(width, 10);
return !isNaN(parsed) && parsed > 0;
} catch {
return false;
}
return Boolean(process.stdout.columns);
}
2 changes: 1 addition & 1 deletion src/widgets/GitBranch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class GitBranchWidget implements Widget {

private getGitBranch(): string | null {
try {
const branch = execSync('git branch --show-current 2>/dev/null', {
const branch = execSync('git branch --show-current', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
Expand Down
4 changes: 2 additions & 2 deletions src/widgets/GitChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ export class GitChangesWidget implements Widget {
let totalInsertions = 0;
let totalDeletions = 0;

const unstagedStat = execSync('git diff --shortstat 2>/dev/null', {
const unstagedStat = execSync('git diff --shortstat', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();

const stagedStat = execSync('git diff --cached --shortstat 2>/dev/null', {
const stagedStat = execSync('git diff --cached --shortstat', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
Expand Down