diff --git a/src/core/browser.test.ts b/src/core/browser.test.ts new file mode 100644 index 0000000..f17b0db --- /dev/null +++ b/src/core/browser.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect, vi, afterEach } from 'vitest' +import { openGenerationInBrowser } from './browser.js' + +const launchMock = vi.fn<(target: string) => Promise>() + +afterEach(() => { + launchMock.mockReset() +}) + +describe('openGenerationInBrowser', () => { + it('opens the generation URL and reports success', async () => { + launchMock.mockResolvedValueOnce() + const result = await openGenerationInBrowser('abc123', launchMock) + + expect(launchMock).toHaveBeenCalledWith('https://www.pixelmuse.studio/g/abc123') + expect(result).toEqual({ ok: true, message: null }) + }) + + it('returns friendly error when browser launch fails', async () => { + launchMock.mockRejectedValueOnce(new Error('no xdg-open')) + const result = await openGenerationInBrowser('abc123', launchMock) + + expect(launchMock).toHaveBeenCalledWith('https://www.pixelmuse.studio/g/abc123') + expect(result.ok).toBe(false) + expect(result.message).toContain('Failed to open browser') + expect(result.message).toContain('https://www.pixelmuse.studio/g/abc123') + }) +}) diff --git a/src/core/browser.ts b/src/core/browser.ts new file mode 100644 index 0000000..e278ae9 --- /dev/null +++ b/src/core/browser.ts @@ -0,0 +1,27 @@ +import open from 'open' + +const GENERATION_URL_BASE = 'https://www.pixelmuse.studio/g/' +const OPEN_BROWSER_ERROR = 'Failed to open browser. Open manually: ' + +export type OpenGenerationResult = + | { ok: true; message: null } + | { ok: false; message: string } + +export type OpenUrl = (url: string) => Promise + +async function openUrl(url: string): Promise { + await open(url) +} + +export async function openGenerationInBrowser( + generationId: string, + launch: OpenUrl = openUrl, +): Promise { + const url = `${GENERATION_URL_BASE}${encodeURIComponent(generationId)}` + try { + await launch(url) + return { ok: true, message: null } + } catch { + return { ok: false, message: `${OPEN_BROWSER_ERROR}${url}` } + } +} diff --git a/src/core/index.ts b/src/core/index.ts index c1d5d55..133e8c5 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 { openGenerationInBrowser } from './browser.js' diff --git a/src/screens/GalleryDetail.tsx b/src/screens/GalleryDetail.tsx index 5953137..db74757 100644 --- a/src/screens/GalleryDetail.tsx +++ b/src/screens/GalleryDetail.tsx @@ -1,10 +1,10 @@ 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 { imageToBuffer, autoSave } from '../core/image.js' +import { openGenerationInBrowser } from '../core/browser.js' import ImagePreview from '../components/ImagePreview.js' interface Props { @@ -45,11 +45,18 @@ export default function GalleryDetail({ client, generationId, back }: Props) { } }, [generationId]) + const handleOpenInBrowser = async (id: string) => { + const result = await openGenerationInBrowser(id) + if (!result.ok && result.message) { + setError(result.message) + } + } + useInput((input) => { if (confirming) return if (input === 'd') setConfirming(true) if (input === 'o' && generation) { - open(`https://www.pixelmuse.studio/g/${generation.id}`) + void handleOpenInBrowser(generation.id) } })