commit 34583f3464ede2612ffc906a0bbe46437167feff
parent dd9d7d30ab0380a8f399f9003a36510c0b6d841a
Author: finwo <finwo@pm.me>
Date: Sat, 21 Mar 2026 05:53:13 +0100
Add bat mode for highlighting
Diffstat:
| M | vecfinder.js | | | 143 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------ |
1 file changed, 111 insertions(+), 32 deletions(-)
diff --git a/vecfinder.js b/vecfinder.js
@@ -6,6 +6,7 @@ const http = require('node:http');
const readline = require('node:readline');
const { stdin, stdout, stderr } = process;
const EventEmitter = require('node:events');
+const { execSync, spawn } = require('node:child_process');
// Configuration
const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.config', 'vecfinder');
@@ -27,6 +28,32 @@ async function loadConfig() {
} catch {}
}
+let hasBat = null;
+
+function checkBat() {
+ if (hasBat !== null) return hasBat;
+ try {
+ execSync('which bat', { stdio: 'ignore' });
+ hasBat = true;
+ } catch {
+ hasBat = false;
+ }
+ return hasBat;
+}
+
+function stripAnsi(str) {
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
+}
+
+async function fileExists(filePath) {
+ try {
+ await fs.access(filePath);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
// Cache
function cachePath(filePath) {
return path.join(CACHE_DIR, filePath);
@@ -271,7 +298,7 @@ async function indexDirectory(store, dirPath) {
// UI
class UI {
- constructor(store) {
+ constructor(store, useBatFlag = null) {
this.store = store;
this.query = '';
this.selectedIndex = 0;
@@ -283,6 +310,9 @@ class UI {
this._previewWidth = null;
this.spinnerFrame = 0;
this.spinnerInterval = null;
+ this.useBat = useBatFlag !== null ? useBatFlag : checkBat();
+ this._batOutput = null;
+ this._batFileId = null;
this.store.on('documentAdded', () => this.render());
this.store.on('queryUpdated', () => {
@@ -325,6 +355,13 @@ class UI {
this.listOffset = Math.max(0, Math.min(this.listOffset, Math.max(0, results.length - maxHeight)));
}
+ getMaxPreviewOffset(maxHeight) {
+ if (this.useBat && this._batOutput) {
+ return Math.max(0, this._batOutput.split('\n').length - maxHeight);
+ }
+ return this._previewWrapped ? Math.max(0, this._previewWrapped.length - maxHeight) : 0;
+ }
+
handleKeyPress(str, key) {
if (!key) return;
@@ -338,7 +375,7 @@ class UI {
this.selectedIndex--;
this.previewOffset = 0;
const height = process.stdout.rows || 24;
- const maxHeight = height - 4;
+ const maxHeight = height - 7;
this.adjustListOffset(maxHeight);
this.render();
}
@@ -351,7 +388,7 @@ class UI {
this.selectedIndex++;
this.previewOffset = 0;
const height = process.stdout.rows || 24;
- const maxHeight = height - 4;
+ const maxHeight = height - 7;
this.adjustListOffset(maxHeight);
this.render();
}
@@ -362,7 +399,7 @@ class UI {
this.selectedIndex = 0;
this.previewOffset = 0;
const height = process.stdout.rows || 24;
- const maxHeight = height - 4;
+ const maxHeight = height - 7;
this.adjustListOffset(maxHeight);
this.render();
return;
@@ -373,7 +410,7 @@ class UI {
this.selectedIndex = results.length - 1;
this.previewOffset = 0;
const height = process.stdout.rows || 24;
- const maxHeight = height - 4;
+ const maxHeight = height - 7;
this.adjustListOffset(maxHeight);
this.render();
return;
@@ -387,8 +424,8 @@ class UI {
if (key.name === 'pagedown') {
const height = process.stdout.rows || 24;
- const previewHeight = height - 4;
- const maxPreviewOffset = this._previewWrapped ? Math.max(0, this._previewWrapped.length - previewHeight) : 0;
+ const maxHeight = height - 7;
+ const maxPreviewOffset = this.getMaxPreviewOffset(maxHeight);
this.previewOffset = Math.min(maxPreviewOffset, this.previewOffset + 10);
this.render();
return;
@@ -426,25 +463,44 @@ class UI {
const leftWidth = Math.floor(width * 0.35);
const rightWidth = width - leftWidth - 1;
- const maxHeight = height - 4;
+ const headerHeight = 3;
+ const maxHeight = height - 4 - headerHeight;
const results = this.store.getResults();
-
- // Header
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
- if (this.store.consumersActive > 0) {
- stderr.write(`${frames[this.spinnerFrame % frames.length]} VecFinder\n`);
+ const spinner = this.store.consumersActive > 0 ? frames[this.spinnerFrame % frames.length] : '\u2713';
+
+ const titleDecorWidth = '┤ VecFinder ├'.length;
+ const dashSpace = leftWidth - titleDecorWidth;
+ const titleCenterLeft = Math.floor(dashSpace / 2);
+ const titleCenterRight = dashSpace - titleCenterLeft;
+
+ // Title bar
+ stderr.write('─'.repeat(titleCenterLeft) + '┤ VecFinder ├' + '─'.repeat(titleCenterRight) + '┬' + '─'.repeat(rightWidth) + '\n');
+
+ // File header line
+ let leftHeader = 'Files ' + spinner;
+ leftHeader = leftHeader.padEnd(leftWidth, ' ').slice(0, leftWidth);
+
+ let rightHeader = '';
+ const selectedResult = results[this.selectedIndex];
+ if (selectedResult) {
+ const title = 'File: ' + selectedResult.id;
+ rightHeader = title.padEnd(rightWidth, ' ').slice(0, rightWidth);
} else {
- stderr.write('\u2713 VecFinder\n');
+ rightHeader = ''.padEnd(rightWidth, ' ');
}
- stderr.write('─'.repeat(leftWidth) + '┬' + '─'.repeat(rightWidth) + '\n');
+
+ stderr.write(leftHeader + '│' + rightHeader + '\n');
+
+ // Separator line
+ stderr.write('─'.repeat(leftWidth) + '┼' + '─'.repeat(rightWidth) + '\n');
// Rows
for (let row = 0; row < maxHeight; row++) {
const result = results[row + this.listOffset];
const isSelected = (row + this.listOffset === this.selectedIndex);
- // Left pane - name left, score right
let left;
if (result) {
let name = result.id;
@@ -461,30 +517,50 @@ class UI {
}
left = left.padEnd(leftWidth, ' ').slice(0, leftWidth);
- // Right pane - preview for selected file, limited by pane height
let right = '';
- const selectedResult = results[this.selectedIndex];
+ const isBatMode = this.useBat && selectedResult?.metadata?.path;
if (selectedResult) {
- if (!this._previewWrapped || this._previewFileId !== selectedResult.id || this._previewWidth !== rightWidth) {
- const lines = selectedResult.content.split('\n');
- this._previewWrapped = [];
- for (const line of lines) {
- if (line.length === 0) {
- this._previewWrapped.push('');
- } else {
- for (let i = 0; i < line.length; i += rightWidth) {
- this._previewWrapped.push(line.slice(i, i + rightWidth));
+ if (isBatMode) {
+ if (!this._batOutput || this._batFileId !== selectedResult.id) {
+ try {
+ this._batOutput = execSync(`bat --color=always --style=numbers --terminal-width=${rightWidth} --wrap=character "${selectedResult.metadata.path}"`, {
+ maxBuffer: 1024 * 1024,
+ encoding: 'utf8',
+ });
+ } catch {
+ this._batOutput = null;
+ }
+ this._batFileId = selectedResult.id;
+ }
+ if (this._batOutput) {
+ const lines = this._batOutput.split('\n');
+ const visibleLines = lines.slice(this.previewOffset, this.previewOffset + maxHeight);
+ const line = visibleLines[row] || '';
+ const stripped = stripAnsi(line);
+ const padding = rightWidth - stripped.length;
+ right = line + ' '.repeat(Math.max(0, padding)) + '\x1b[0m';
+ }
+ } else {
+ if (!this._previewWrapped || this._previewFileId !== selectedResult.id || this._previewWidth !== rightWidth) {
+ const lines = selectedResult.content.split('\n');
+ this._previewWrapped = [];
+ for (const line of lines) {
+ if (line.length === 0) {
+ this._previewWrapped.push('');
+ } else {
+ for (let i = 0; i < line.length; i += rightWidth) {
+ this._previewWrapped.push(line.slice(i, i + rightWidth));
+ }
}
}
+ this._previewFileId = selectedResult.id;
+ this._previewWidth = rightWidth;
}
- this._previewFileId = selectedResult.id;
- this._previewWidth = rightWidth;
+ right = this._previewWrapped[row + this.previewOffset] || '';
+ right = right.padEnd(rightWidth, ' ').slice(0, rightWidth);
}
- right = this._previewWrapped[row + this.previewOffset] || '';
}
- right = right.padEnd(rightWidth, ' ').slice(0, rightWidth);
- // Write
if (isSelected) {
stderr.write(`\x1b[7m${left}\x1b[0m│${right}`);
} else {
@@ -501,11 +577,14 @@ class UI {
// Main
async function main() {
+ const args = process.argv.slice(2);
+ const useBatFlag = args.includes('--no-bat') ? false : null;
+
await loadConfig();
await fs.mkdir(CACHE_DIR, { recursive: true });
const store = new DocumentStore();
- const ui = new UI(store);
+ const ui = new UI(store, useBatFlag);
ui.init();