diff --git a/src/tui/browser/Browser.tsx b/src/tui/browser/Browser.tsx index 5d2ee4a..316f99f 100644 --- a/src/tui/browser/Browser.tsx +++ b/src/tui/browser/Browser.tsx @@ -11,6 +11,7 @@ export interface BrowserProps { writeSelection: (s: Selection) => void; onQuit: () => void; onRescan?: () => void; + onOpenInEditor?: (absPath: string) => void; } interface FlatRow { @@ -32,7 +33,7 @@ function flatten(project: Project, expanded: Set): FlatRow[] { return rows; } -export function Browser({ project, readPou, writeSelection, onQuit, onRescan }: BrowserProps): React.ReactElement { +export function Browser({ project, readPou, writeSelection, onQuit, onRescan, onOpenInEditor }: BrowserProps): React.ReactElement { const [expanded, setExpanded] = React.useState>(new Set()); const [cursorIdx, setCursorIdx] = React.useState(0); const [text, setText] = React.useState(null); @@ -75,6 +76,9 @@ export function Browser({ project, readPou, writeSelection, onQuit, onRescan }: } if (input === 'q') return onQuit(); if (input === 'r' && onRescan) return onRescan(); + if (input === 'o' && onOpenInEditor && cursor?.kind === 'pou' && cursor.pou) { + return onOpenInEditor(cursor.pou.absPath); + } if (input === 'j' || key.downArrow) { setCursorIdx((i) => Math.min(i + 1, rows.length - 1)); } else if (input === 'k' || key.upArrow) { @@ -118,7 +122,7 @@ export function Browser({ project, readPou, writeSelection, onQuit, onRescan }: - j/k nav l expand h collapse r rescan ? help q quit + j/k nav l expand h collapse o open r rescan ? help q quit ); } @@ -131,6 +135,7 @@ function HelpOverlay(): React.ReactElement { k / ↑ move cursor up l / → expand device h / ← collapse device + o open highlighted POU in $EDITOR (or VS Code) r re-scan mcp-mirror/ ? toggle this help Esc close help diff --git a/src/tui/index.tsx b/src/tui/index.tsx index e21f44c..28502fd 100644 --- a/src/tui/index.tsx +++ b/src/tui/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render } from 'ink'; import * as fs from 'fs/promises'; +import { spawn } from 'child_process'; import { Approve, Decision } from './approve/Approve.js'; import { Browser } from './browser/Browser.js'; import { walk } from './shared/scan.js'; @@ -51,6 +52,15 @@ async function runBrowser(maybeRoot: string | undefined): Promise { resolve(0); }; const readPou = (pou: { absPath: string }) => fs.readFile(pou.absPath, 'utf8'); + const onOpenInEditor = (absPath: string) => { + const editor = process.env.EDITOR || 'code'; + try { + const child = spawn(editor, [absPath], { stdio: 'ignore', detached: true, shell: true }); + child.unref(); + } catch (err) { + process.stderr.write(`phobiCS-tui: open-in-editor failed: ${(err as Error).message}\n`); + } + }; const onRescan = async () => { try { const next = await walk(root); @@ -61,6 +71,7 @@ async function runBrowser(maybeRoot: string | undefined): Promise { writeSelection={onWriteSelection} onQuit={onQuit} onRescan={onRescan} + onOpenInEditor={onOpenInEditor} /> ); } catch (err) { @@ -74,6 +85,7 @@ async function runBrowser(maybeRoot: string | undefined): Promise { writeSelection={onWriteSelection} onQuit={onQuit} onRescan={onRescan} + onOpenInEditor={onOpenInEditor} /> ); }); diff --git a/tests/tui/Browser.test.tsx b/tests/tui/Browser.test.tsx index dd53974..044101f 100644 --- a/tests/tui/Browser.test.tsx +++ b/tests/tui/Browser.test.tsx @@ -99,6 +99,44 @@ describe('', () => { expect(lastFrame()).not.toContain('Keybindings'); }); + it('calls onOpenInEditor on o with the highlighted POU absPath', async () => { + const onOpenInEditor = vi.fn(); + const { stdin } = render( + ''} + writeSelection={() => {}} + onQuit={() => {}} + onOpenInEditor={onOpenInEditor} + /> + ); + await flush(); + stdin.write('l'); + await flush(); + stdin.write('j'); + await flush(); + stdin.write('o'); + await flush(); + expect(onOpenInEditor).toHaveBeenCalledWith('/abs/PLC_PRG.st'); + }); + + it('does not call onOpenInEditor when cursor is on a device row', async () => { + const onOpenInEditor = vi.fn(); + const { stdin } = render( + ''} + writeSelection={() => {}} + onQuit={() => {}} + onOpenInEditor={onOpenInEditor} + /> + ); + await flush(); + stdin.write('o'); + await flush(); + expect(onOpenInEditor).not.toHaveBeenCalled(); + }); + it('calls onRescan on r', async () => { const onRescan = vi.fn(); const { stdin } = render(