tui(browser): o opens highlighted POU in $EDITOR (or VS Code)
o on a POU row spawns $EDITOR (defaulting to 'code') with the absolute path. detached + stdio:'ignore' + unref so the editor's lifetime is independent of the TUI; shell:true so PATH lookup works on Windows where 'code' is a .cmd shim. Ignored on device rows (no abs_path).
This commit is contained in:
parent
4d7cbdb6f6
commit
2441a898e8
3 changed files with 57 additions and 2 deletions
|
|
@ -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<string>): 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<Set<string>>(new Set());
|
||||
const [cursorIdx, setCursorIdx] = React.useState(0);
|
||||
const [text, setText] = React.useState<string | null>(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 }:
|
|||
<Viewer pou={cursor?.pou ?? null} text={text} scrollTop={scrollTop} visibleRows={20} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Text>j/k nav l expand h collapse r rescan ? help q quit</Text>
|
||||
<Text>j/k nav l expand h collapse o open r rescan ? help q quit</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -131,6 +135,7 @@ function HelpOverlay(): React.ReactElement {
|
|||
<Text> k / ↑ move cursor up</Text>
|
||||
<Text> l / → expand device</Text>
|
||||
<Text> h / ← collapse device</Text>
|
||||
<Text> o open highlighted POU in $EDITOR (or VS Code)</Text>
|
||||
<Text> r re-scan mcp-mirror/</Text>
|
||||
<Text> ? toggle this help</Text>
|
||||
<Text> Esc close help</Text>
|
||||
|
|
|
|||
|
|
@ -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<number> {
|
|||
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<number> {
|
|||
writeSelection={onWriteSelection}
|
||||
onQuit={onQuit}
|
||||
onRescan={onRescan}
|
||||
onOpenInEditor={onOpenInEditor}
|
||||
/>
|
||||
);
|
||||
} catch (err) {
|
||||
|
|
@ -74,6 +85,7 @@ async function runBrowser(maybeRoot: string | undefined): Promise<number> {
|
|||
writeSelection={onWriteSelection}
|
||||
onQuit={onQuit}
|
||||
onRescan={onRescan}
|
||||
onOpenInEditor={onOpenInEditor}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,6 +99,44 @@ describe('<Browser>', () => {
|
|||
expect(lastFrame()).not.toContain('Keybindings');
|
||||
});
|
||||
|
||||
it('calls onOpenInEditor on o with the highlighted POU absPath', async () => {
|
||||
const onOpenInEditor = vi.fn();
|
||||
const { stdin } = render(
|
||||
<Browser
|
||||
project={project}
|
||||
readPou={async () => ''}
|
||||
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(
|
||||
<Browser
|
||||
project={project}
|
||||
readPou={async () => ''}
|
||||
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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue