vecfinder

Document searching tool based on embedding vectors
git clone git://git.finwo.net/app/vecfinder
Log | Files | Refs

commit 72066aa741afccc5b9a7fdd8ba7a79f686e3dc0f
parent 34583f3464ede2612ffc906a0bbe46437167feff
Author: finwo <finwo@pm.me>
Date:   Sat, 21 Mar 2026 16:02:46 +0100

Fix redraw flickering when idle

Diffstat:
Mvecfinder.js | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 58 insertions(+), 9 deletions(-)

diff --git a/vecfinder.js b/vecfinder.js @@ -313,12 +313,18 @@ class UI { this.useBat = useBatFlag !== null ? useBatFlag : checkBat(); this._batOutput = null; this._batFileId = null; + this._lastRenderState = null; + this._needsFullRedraw = false; - this.store.on('documentAdded', () => this.render()); + this.store.on('documentAdded', () => { + this._needsFullRedraw = true; + this.render(); + }); this.store.on('queryUpdated', () => { this.selectedIndex = 0; this.listOffset = 0; this.previewOffset = 0; + this._needsFullRedraw = true; this.render(); }); } @@ -456,8 +462,6 @@ class UI { } render() { - stderr.write('\x1b[2J\x1b[H'); - const width = process.stdout.columns || 80; const height = process.stdout.rows || 24; @@ -475,15 +479,63 @@ class UI { const titleCenterLeft = Math.floor(dashSpace / 2); const titleCenterRight = dashSpace - titleCenterLeft; - // Title bar + const selectedResult = results[this.selectedIndex]; + + const visibleIds = []; + for (let row = 0; row < maxHeight; row++) { + const result = results[row + this.listOffset]; + visibleIds.push(result ? result.id : null); + } + + const currentState = { + width, + height, + leftWidth, + rightWidth, + query: this.query, + selectedIndex: this.selectedIndex, + listOffset: this.listOffset, + previewOffset: this.previewOffset, + spinner, + consumersActive: this.store.consumersActive, + selectedFileId: selectedResult?.id, + visibleIds, + batFileId: this._batFileId, + batOutputHash: this._batOutput ? this._batOutput.slice(0, 100) : null, + }; + + if (!this._needsFullRedraw && this._lastRenderState) { + const s = this._lastRenderState; + if ( + s.width === currentState.width && + s.height === currentState.height && + s.leftWidth === currentState.leftWidth && + s.rightWidth === currentState.rightWidth && + s.query === currentState.query && + s.selectedIndex === currentState.selectedIndex && + s.listOffset === currentState.listOffset && + s.previewOffset === currentState.previewOffset && + s.spinner === currentState.spinner && + s.consumersActive === currentState.consumersActive && + s.selectedFileId === currentState.selectedFileId && + s.visibleIds.length === currentState.visibleIds.length && + s.visibleIds.every((id, i) => id === currentState.visibleIds[i]) && + s.batFileId === currentState.batFileId + ) { + return; + } + } + this._needsFullRedraw = false; + this._lastRenderState = currentState; + + stderr.write('\x1b[2J\x1b[H'); + 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); @@ -493,10 +545,8 @@ class UI { 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); @@ -569,7 +619,6 @@ class UI { stderr.write('\n'); } - // Footer stderr.write('─'.repeat(leftWidth) + '┴' + '─'.repeat(rightWidth) + '\n'); stderr.write(`Query: ${this.query}`); }