mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-09-02 18:18:38 +00:00
Add 'star us on github' menu item, version bump to 2.1.2
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ccstatusline",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"description": "A customizable status line formatter for Claude Code CLI",
|
||||
"module": "src/ccstatusline.ts",
|
||||
"type": "module",
|
||||
|
||||
+46
-11
@@ -28,6 +28,7 @@ import {
|
||||
loadSettings,
|
||||
saveSettings
|
||||
} from '../utils/config';
|
||||
import { openExternalUrl } from '../utils/open-url';
|
||||
import {
|
||||
checkPowerlineFonts,
|
||||
checkPowerlineFontsAsync,
|
||||
@@ -50,6 +51,13 @@ import {
|
||||
TerminalWidthMenu
|
||||
} from './components';
|
||||
|
||||
const GITHUB_REPO_URL = 'https://github.com/sirmalloc/ccstatusline';
|
||||
|
||||
interface FlashMessage {
|
||||
text: string;
|
||||
color: 'green' | 'red';
|
||||
}
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const { exit } = useApp();
|
||||
const [settings, setSettings] = useState<Settings | null>(null);
|
||||
@@ -65,7 +73,7 @@ export const App: React.FC = () => {
|
||||
const [installingFonts, setInstallingFonts] = useState(false);
|
||||
const [fontInstallMessage, setFontInstallMessage] = useState<string | null>(null);
|
||||
const [existingStatusLine, setExistingStatusLine] = useState<string | null>(null);
|
||||
const [saveMessage, setSaveMessage] = useState<string | null>(null);
|
||||
const [flashMessage, setFlashMessage] = useState<FlashMessage | null>(null);
|
||||
const [previewIsTruncated, setPreviewIsTruncated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -107,15 +115,15 @@ export const App: React.FC = () => {
|
||||
}
|
||||
}, [settings, originalSettings]);
|
||||
|
||||
// Clear save message after 2 seconds
|
||||
// Clear header message after 2 seconds
|
||||
useEffect(() => {
|
||||
if (saveMessage) {
|
||||
if (flashMessage) {
|
||||
const timer = setTimeout(() => {
|
||||
setSaveMessage(null);
|
||||
setFlashMessage(null);
|
||||
}, 2000);
|
||||
return () => { clearTimeout(timer); };
|
||||
}
|
||||
}, [saveMessage]);
|
||||
}, [flashMessage]);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.ctrl && input === 'c') {
|
||||
@@ -127,7 +135,10 @@ export const App: React.FC = () => {
|
||||
await saveSettings(settings);
|
||||
setOriginalSettings(JSON.parse(JSON.stringify(settings)) as Settings);
|
||||
setHasChanges(false);
|
||||
setSaveMessage('✓ Configuration saved');
|
||||
setFlashMessage({
|
||||
text: '✓ Configuration saved',
|
||||
color: 'green'
|
||||
});
|
||||
})();
|
||||
}
|
||||
});
|
||||
@@ -211,6 +222,29 @@ export const App: React.FC = () => {
|
||||
case 'install':
|
||||
handleInstallUninstall();
|
||||
break;
|
||||
case 'starGithub':
|
||||
setConfirmDialog({
|
||||
message: `Open the ccstatusline GitHub repository in your browser?\n\n${GITHUB_REPO_URL}`,
|
||||
action: () => {
|
||||
const result = openExternalUrl(GITHUB_REPO_URL);
|
||||
if (result.success) {
|
||||
setFlashMessage({
|
||||
text: '✓ Opened GitHub repository in browser',
|
||||
color: 'green'
|
||||
});
|
||||
} else {
|
||||
setFlashMessage({
|
||||
text: `✗ Could not open browser. Visit: ${GITHUB_REPO_URL}`,
|
||||
color: 'red'
|
||||
});
|
||||
}
|
||||
setScreen('main');
|
||||
setConfirmDialog(null);
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
setScreen('confirm');
|
||||
break;
|
||||
case 'save':
|
||||
await saveSettings(settings);
|
||||
setOriginalSettings(JSON.parse(JSON.stringify(settings)) as Settings); // Update original after save
|
||||
@@ -249,9 +283,9 @@ export const App: React.FC = () => {
|
||||
<Text bold>
|
||||
{` | ${getPackageVersion() && `v${getPackageVersion()}`}`}
|
||||
</Text>
|
||||
{saveMessage && (
|
||||
<Text color='green' bold>
|
||||
{` ${saveMessage}`}
|
||||
{flashMessage && (
|
||||
<Text color={flashMessage.color} bold>
|
||||
{` ${flashMessage.text}`}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
@@ -275,7 +309,8 @@ export const App: React.FC = () => {
|
||||
powerline: 2,
|
||||
terminalConfig: 3,
|
||||
globalOverrides: 4,
|
||||
install: 5
|
||||
install: 5,
|
||||
starGithub: hasChanges ? 8 : 7
|
||||
};
|
||||
setMenuSelections({ ...menuSelections, main: menuMap[value] ?? 0 });
|
||||
}
|
||||
@@ -460,4 +495,4 @@ export function runTUI() {
|
||||
// Clear the terminal before starting the TUI
|
||||
process.stdout.write('\x1b[2J\x1b[H');
|
||||
render(<App />);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,16 @@ export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled,
|
||||
if (hasChanges) {
|
||||
menuItems.push(
|
||||
{ label: '💾 Save & Exit', value: 'save', selectable: true },
|
||||
{ label: '❌ Exit without saving', value: 'exit', selectable: true }
|
||||
{ label: '❌ Exit without saving', value: 'exit', selectable: true },
|
||||
{ label: '', value: '_gap3', selectable: false }, // Visual gap
|
||||
{ label: '⭐ Like ccstatusline? Star us on GitHub', value: 'starGithub', selectable: true }
|
||||
);
|
||||
} else {
|
||||
menuItems.push({ label: '🚪 Exit', value: 'exit', selectable: true });
|
||||
menuItems.push(
|
||||
{ label: '🚪 Exit', value: 'exit', selectable: true },
|
||||
{ label: '', value: '_gap3', selectable: false }, // Visual gap
|
||||
{ label: '⭐ Like ccstatusline? Star us on GitHub', value: 'starGithub', selectable: true }
|
||||
);
|
||||
}
|
||||
|
||||
// Get only selectable items for navigation
|
||||
@@ -69,6 +75,7 @@ export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled,
|
||||
? 'Remove ccstatusline from your Claude Code settings'
|
||||
: 'Add ccstatusline to your Claude Code settings for automatic status line rendering',
|
||||
terminalConfig: 'Configure terminal-specific settings for optimal display',
|
||||
starGithub: 'Open the ccstatusline GitHub repository in your browser so you can star the project',
|
||||
save: 'Save all changes and exit the configuration tool',
|
||||
exit: hasChanges
|
||||
? 'Exit without saving your changes'
|
||||
@@ -117,4 +124,4 @@ export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled,
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as os from 'os';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import { openExternalUrl } from '../open-url';
|
||||
|
||||
vi.mock('child_process', () => ({ spawnSync: vi.fn() }));
|
||||
|
||||
const mockSpawnSync = spawnSync as unknown as {
|
||||
mock: { calls: unknown[][] };
|
||||
mockReturnValue: (value: unknown) => void;
|
||||
mockReturnValueOnce: (value: unknown) => void;
|
||||
};
|
||||
|
||||
describe('openExternalUrl', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('uses open on macOS', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('darwin');
|
||||
mockSpawnSync.mockReturnValue({ status: 0 });
|
||||
|
||||
const result = openExternalUrl('https://github.com/sirmalloc/ccstatusline');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockSpawnSync.mock.calls[0]?.[0]).toBe('open');
|
||||
expect(mockSpawnSync.mock.calls[0]?.[1]).toEqual(['https://github.com/sirmalloc/ccstatusline']);
|
||||
});
|
||||
|
||||
it('uses cmd start on Windows', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('win32');
|
||||
mockSpawnSync.mockReturnValue({ status: 0 });
|
||||
|
||||
const result = openExternalUrl('https://github.com/sirmalloc/ccstatusline');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockSpawnSync.mock.calls[0]?.[0]).toBe('cmd');
|
||||
expect(mockSpawnSync.mock.calls[0]?.[1]).toEqual(['/c', 'start', '', 'https://github.com/sirmalloc/ccstatusline']);
|
||||
});
|
||||
|
||||
it('uses xdg-open on Linux when available', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('linux');
|
||||
mockSpawnSync.mockReturnValue({ status: 0 });
|
||||
|
||||
const result = openExternalUrl('https://github.com/sirmalloc/ccstatusline');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockSpawnSync.mock.calls[0]?.[0]).toBe('xdg-open');
|
||||
expect(mockSpawnSync.mock.calls[0]?.[1]).toEqual(['https://github.com/sirmalloc/ccstatusline']);
|
||||
});
|
||||
|
||||
it('falls back to gio open when xdg-open fails', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('linux');
|
||||
mockSpawnSync.mockReturnValueOnce({ status: 1 });
|
||||
mockSpawnSync.mockReturnValueOnce({ status: 0 });
|
||||
|
||||
const result = openExternalUrl('https://github.com/sirmalloc/ccstatusline');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockSpawnSync.mock.calls.length).toBe(2);
|
||||
expect(mockSpawnSync.mock.calls[0]?.[0]).toBe('xdg-open');
|
||||
expect(mockSpawnSync.mock.calls[1]?.[0]).toBe('gio');
|
||||
expect(mockSpawnSync.mock.calls[1]?.[1]).toEqual(['open', 'https://github.com/sirmalloc/ccstatusline']);
|
||||
});
|
||||
|
||||
it('returns failure when Linux openers fail', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('linux');
|
||||
mockSpawnSync.mockReturnValueOnce({ status: 1 });
|
||||
mockSpawnSync.mockReturnValueOnce({ status: 2 });
|
||||
|
||||
const result = openExternalUrl('https://github.com/sirmalloc/ccstatusline');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('xdg-open failed');
|
||||
expect(result.error).toContain('gio open failed');
|
||||
});
|
||||
|
||||
it('rejects non-http URL protocols', () => {
|
||||
const result = openExternalUrl('file:///tmp/ccstatusline');
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'Only http(s) URLs are supported'
|
||||
});
|
||||
expect(mockSpawnSync.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('returns unsupported platform error', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('freebsd');
|
||||
|
||||
const result = openExternalUrl('https://github.com/sirmalloc/ccstatusline');
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'Unsupported platform: freebsd'
|
||||
});
|
||||
expect(mockSpawnSync.mock.calls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as os from 'os';
|
||||
|
||||
export interface OpenExternalUrlResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function runOpenCommand(command: string, args: string[]): string | null {
|
||||
const result = spawnSync(command, args, {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
return result.error.message;
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
return `Command exited with status ${result.status}`;
|
||||
}
|
||||
|
||||
if (result.signal) {
|
||||
return `Command terminated by signal ${result.signal}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function openExternalUrl(url: string): OpenExternalUrlResult {
|
||||
let parsedUrl: URL;
|
||||
|
||||
try {
|
||||
parsedUrl = new URL(url);
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid URL'
|
||||
};
|
||||
}
|
||||
|
||||
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Only http(s) URLs are supported'
|
||||
};
|
||||
}
|
||||
|
||||
const platform = os.platform();
|
||||
|
||||
if (platform === 'darwin') {
|
||||
const commandError = runOpenCommand('open', [url]);
|
||||
return commandError ? { success: false, error: commandError } : { success: true };
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
const commandError = runOpenCommand('cmd', ['/c', 'start', '', url]);
|
||||
return commandError ? { success: false, error: commandError } : { success: true };
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
const xdgError = runOpenCommand('xdg-open', [url]);
|
||||
if (!xdgError) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const gioError = runOpenCommand('gio', ['open', url]);
|
||||
if (!gioError) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `xdg-open failed: ${xdgError}; gio open failed: ${gioError}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Unsupported platform: ${platform}`
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user