diff --git a/src/core/browser.test.ts b/src/core/browser.test.ts new file mode 100644 index 0000000..42df3f4 --- /dev/null +++ b/src/core/browser.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect, vi } from 'vitest' +import { openInBrowser } from './browser.js' + +describe('openInBrowser', () => { + it('calls provided opener with the target URL', async () => { + const openUrl = vi.fn(async (_url: string) => undefined) + const url = 'https://www.pixelmuse.studio/g/abc123' + + await openInBrowser(url, { openUrl }) + + expect(openUrl).toHaveBeenCalledTimes(1) + expect(openUrl).toHaveBeenCalledWith(url) + }) + + it('propagates opener failures to caller', async () => { + const openUrl = vi.fn(async (_url: string) => { + throw new Error('No browser available') + }) + + await expect( + openInBrowser('https://www.pixelmuse.studio/g/abc123', { openUrl }), + ).rejects.toThrow('No browser available') + }) +}) diff --git a/src/core/browser.ts b/src/core/browser.ts new file mode 100644 index 0000000..0a09a90 --- /dev/null +++ b/src/core/browser.ts @@ -0,0 +1,13 @@ +import open from 'open' + +interface OpenInBrowserOptions { + openUrl?: (url: string) => Promise +} + +/** + * Open a URL in the system browser. + */ +export async function openInBrowser(url: string, options: OpenInBrowserOptions = {}): Promise { + const openUrl = options.openUrl ?? ((target: string) => open(target)) + await openUrl(url) +} diff --git a/src/core/index.ts b/src/core/index.ts index c1d5d55..a7e5be7 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -38,3 +38,4 @@ export { timeAgo } from './utils.js' export { initiateDeviceAuth, pollForToken } from './device-auth.js' export { detectEditors, configureMcp, type EditorInfo } from './mcp-config.js' export { buildMacClipboardArgs } from './clipboard.js' +export { openInBrowser } from './browser.js' diff --git a/src/screens/GalleryDetail.tsx b/src/screens/GalleryDetail.tsx index 5953137..330b216 100644 --- a/src/screens/GalleryDetail.tsx +++ b/src/screens/GalleryDetail.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react' import { Box, Text, useInput } from 'ink' import { ConfirmInput, Spinner } from '@inkjs/ui' -import open from 'open' import type { PixelmuseClient } from '../core/client.js' import type { Generation } from '../core/types.js' +import { openInBrowser } from '../core/browser.js' import { imageToBuffer, autoSave } from '../core/image.js' import ImagePreview from '../components/ImagePreview.js' @@ -49,7 +49,9 @@ export default function GalleryDetail({ client, generationId, back }: Props) { if (confirming) return if (input === 'd') setConfirming(true) if (input === 'o' && generation) { - open(`https://www.pixelmuse.studio/g/${generation.id}`) + void openInBrowser(`https://www.pixelmuse.studio/g/${generation.id}`).catch((err: unknown) => { + setError(err instanceof Error ? err.message : 'Failed to open browser') + }) } })