131 lines
5.0 KiB
JavaScript
131 lines
5.0 KiB
JavaScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { Box, Text, useInput } from 'ink';
|
|
import path from 'path';
|
|
|
|
const h = React.createElement;
|
|
|
|
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
|
|
|
const renderTabTitle = (tab, maxLen) => {
|
|
const base = tab.title || path.basename(tab.path || '') || 'untitled';
|
|
if (base.length <= maxLen) return base;
|
|
return base.slice(0, Math.max(1, maxLen - 1)) + '…';
|
|
};
|
|
|
|
const FilePreviewTabs = ({
|
|
tabs = [],
|
|
activeId = null,
|
|
onActivate,
|
|
onClose,
|
|
isActive = false,
|
|
width = 80,
|
|
height = 10
|
|
}) => {
|
|
const activeTab = tabs.find(t => t.id === activeId) || tabs[0] || null;
|
|
|
|
const [scrollTop, setScrollTop] = useState(0);
|
|
|
|
useEffect(() => {
|
|
setScrollTop(0);
|
|
}, [activeId]);
|
|
|
|
const contentLines = useMemo(() => {
|
|
if (!activeTab?.content) return [];
|
|
return activeTab.content.replace(/\r\n/g, '\n').split('\n');
|
|
}, [activeTab?.content]);
|
|
|
|
const headerRows = 2;
|
|
const footerRows = 1;
|
|
const bodyRows = Math.max(3, height - headerRows - footerRows);
|
|
|
|
const maxScroll = Math.max(0, contentLines.length - bodyRows);
|
|
const safeScrollTop = clamp(scrollTop, 0, maxScroll);
|
|
|
|
useEffect(() => {
|
|
if (safeScrollTop !== scrollTop) setScrollTop(safeScrollTop);
|
|
}, [safeScrollTop, scrollTop]);
|
|
|
|
useInput((input, key) => {
|
|
if (!isActive) return;
|
|
if (!activeTab) return;
|
|
|
|
if (key.escape) {
|
|
return;
|
|
}
|
|
|
|
if (key.upArrow) setScrollTop(v => Math.max(0, v - 1));
|
|
if (key.downArrow) setScrollTop(v => Math.min(maxScroll, v + 1));
|
|
if (key.pageUp) setScrollTop(v => Math.max(0, v - bodyRows));
|
|
if (key.pageDown) setScrollTop(v => Math.min(maxScroll, v + bodyRows));
|
|
if (key.home) setScrollTop(0);
|
|
if (key.end) setScrollTop(maxScroll);
|
|
|
|
if (key.leftArrow) {
|
|
const idx = tabs.findIndex(t => t.id === activeTab.id);
|
|
const prev = idx > 0 ? tabs[idx - 1] : tabs[tabs.length - 1];
|
|
if (prev && typeof onActivate === 'function') onActivate(prev.id);
|
|
}
|
|
if (key.rightArrow) {
|
|
const idx = tabs.findIndex(t => t.id === activeTab.id);
|
|
const next = idx >= 0 && idx < tabs.length - 1 ? tabs[idx + 1] : tabs[0];
|
|
if (next && typeof onActivate === 'function') onActivate(next.id);
|
|
}
|
|
|
|
if (key.ctrl && input.toLowerCase() === 'w') {
|
|
if (typeof onClose === 'function') onClose(activeTab.id);
|
|
}
|
|
}, { isActive });
|
|
|
|
const tabRow = useMemo(() => {
|
|
if (tabs.length === 0) return '';
|
|
const pad = 1;
|
|
const maxTitleLen = Math.max(6, Math.floor(width / Math.max(1, tabs.length)) - 6);
|
|
const parts = tabs.map(t => {
|
|
const title = renderTabTitle(t, maxTitleLen);
|
|
const dirty = t.dirty ? '*' : '';
|
|
return (t.id === activeId ? `[${title}${dirty}]` : ` ${title}${dirty} `);
|
|
});
|
|
const joined = parts.join(' ');
|
|
const truncated = joined.length > width - pad ? joined.slice(0, Math.max(0, width - pad - 1)) + '…' : joined;
|
|
return truncated;
|
|
}, [tabs, activeId, width]);
|
|
|
|
const lineNoWidth = Math.max(4, String(safeScrollTop + bodyRows).length + 1);
|
|
const contentWidth = Math.max(10, width - lineNoWidth - 2);
|
|
|
|
const visible = contentLines.slice(safeScrollTop, safeScrollTop + bodyRows);
|
|
|
|
return h(Box, {
|
|
flexDirection: 'column',
|
|
width,
|
|
height,
|
|
borderStyle: 'single',
|
|
borderColor: isActive ? 'cyan' : 'gray'
|
|
},
|
|
h(Box, { paddingX: 1, justifyContent: 'space-between' },
|
|
h(Text, { color: 'cyan', bold: true }, 'Files'),
|
|
h(Text, { color: 'gray', dimColor: true }, isActive ? '↑↓ scroll ←→ tabs Ctrl+W close Esc focus chat' : 'Ctrl+O open Ctrl+Shift+F search Tab focus')
|
|
),
|
|
h(Box, { paddingX: 1 },
|
|
h(Text, { color: 'white', wrap: 'truncate-end' }, tabRow || '(no tabs)')
|
|
),
|
|
h(Box, { flexDirection: 'column', flexGrow: 1, paddingX: 1 },
|
|
activeTab ? visible.map((line, i) => {
|
|
const lineNo = safeScrollTop + i + 1;
|
|
const no = String(lineNo).padStart(lineNoWidth - 1) + ' ';
|
|
return h(Box, { key: `${activeTab.id}:${lineNo}`, width: '100%' },
|
|
h(Text, { color: 'gray', dimColor: true }, no),
|
|
h(Text, { color: 'white', wrap: 'truncate-end' }, (line || '').slice(0, contentWidth))
|
|
);
|
|
}) : h(Text, { color: 'gray', dimColor: true }, 'Open a file to preview it here.')
|
|
),
|
|
h(Box, { paddingX: 1, justifyContent: 'space-between' },
|
|
h(Text, { color: 'gray', dimColor: true, wrap: 'truncate' }, activeTab?.relPath ? activeTab.relPath : ''),
|
|
activeTab ? h(Text, { color: 'gray', dimColor: true }, `${safeScrollTop + 1}-${Math.min(contentLines.length, safeScrollTop + bodyRows)} / ${contentLines.length}`) : null
|
|
)
|
|
);
|
|
};
|
|
|
|
export default FilePreviewTabs;
|
|
|