From 9902cd6dcf999962a73e464104297cf6fc6b391c Mon Sep 17 00:00:00 2001 From: Karstein Phobic Nyvold Kvistad Date: Tue, 28 Apr 2026 22:21:05 +0200 Subject: [PATCH] tui: add Tree component (collapsible device/POU listing) renders the project as a two-level collapsible list: device headers with POU count, and (when expanded) POU rows with name + kind + LOC count. Cursor row marked with a leading triangle. devicePath()/pouPath() are exported keypath helpers used by the tree state machine to address rows uniformly. --- src/tui/browser/Tree.tsx | 44 ++++++++++++++++++++++++++++++ tests/tui/Tree.test.tsx | 58 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/tui/browser/Tree.tsx create mode 100644 tests/tui/Tree.test.tsx diff --git a/src/tui/browser/Tree.tsx b/src/tui/browser/Tree.tsx new file mode 100644 index 0000000..862c712 --- /dev/null +++ b/src/tui/browser/Tree.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Box, Text } from 'ink'; +import { Project, POU } from '../shared/types.js'; + +export interface TreeProps { + project: Project; + cursorPath: string; + expanded: Set; +} + +export function devicePath(deviceName: string): string { + return `device:${deviceName}`; +} +export function pouPath(deviceName: string, relPath: string): string { + return `pou:${deviceName}:${relPath}`; +} + +export function Tree({ project, cursorPath, expanded }: TreeProps): React.ReactElement { + const rows: React.ReactElement[] = []; + for (const dev of project.devices) { + const dPath = devicePath(dev.name); + const isExpanded = expanded.has(dPath); + const isCursor = cursorPath === dPath; + rows.push( + + {isCursor ? '▶ ' : ' '} + {isExpanded ? '▾ ' : '▸ '} + {dev.name} {dev.pous.length} POUs + + ); + if (!isExpanded) continue; + for (const p of dev.pous) { + const pPath = pouPath(dev.name, p.relPath); + const isPCursor = cursorPath === pPath; + rows.push( + + {isPCursor ? '▶ ' : ' '} + {p.name.padEnd(18)} {p.kind.padEnd(6)} {String(p.loc).padStart(4)} L + + ); + } + } + return {rows}; +} diff --git a/tests/tui/Tree.test.tsx b/tests/tui/Tree.test.tsx new file mode 100644 index 0000000..6a416c7 --- /dev/null +++ b/tests/tui/Tree.test.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { describe, it, expect } from 'vitest'; +import { render } from 'ink-testing-library'; +import { Tree } from '../../src/tui/browser/Tree.tsx'; +import { Project } from '../../src/tui/shared/types.ts'; + +const project: Project = { + rootDir: '/p', + mirrorMtimeMs: 0, + devices: [ + { + name: 'CodesysRpi', + pous: [ + { name: 'PLC_PRG', kind: 'PRG', relPath: 'a/PLC_PRG.st', absPath: '/abs/a/PLC_PRG.st', loc: 5, mtimeMs: 0 }, + { name: 'FB_Test', kind: 'FB', relPath: 'a/FB_Test.st', absPath: '/abs/a/FB_Test.st', loc: 87, mtimeMs: 0 }, + ], + }, + ], +}; + +describe('', () => { + it('renders devices and POU rows when expanded', () => { + const { lastFrame } = render( + + ); + const out = lastFrame()!; + expect(out).toContain('CodesysRpi'); + expect(out).toContain('PLC_PRG'); + expect(out).toContain('FB_Test'); + expect(out).toContain('PRG'); + expect(out).toContain('FB'); + }); + + it('does not show POU rows when device is collapsed', () => { + const { lastFrame } = render( + + ); + const out = lastFrame()!; + expect(out).toContain('CodesysRpi'); + expect(out).not.toContain('PLC_PRG'); + }); + + it('marks the cursor row', () => { + const { lastFrame } = render( + + ); + const out = lastFrame()!; + expect(out).toMatch(/▶ FB_Test/); + }); +});