Skip to content

Commit 13bb8e8

Browse files
ozgesolidkeyclaude
andcommitted
Add keyboard navigation, word wrap, and fix minimap sync
- Arrow keys: up/down to move lines, left/right to scroll horizontally - Page Up/Down, Home/End for fast navigation - Word wrap toggle: Wrap button in toolbar or Option+Z - Fix minimap color alignment with actual log lines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f32db17 commit 13bb8e8

3 files changed

Lines changed: 139 additions & 5 deletions

File tree

src/renderer/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<button id="btn-split" class="toolbar-btn" title="Split file into parts" disabled>Split</button>
4141
<button id="btn-analyze" class="toolbar-btn" title="Analyze file" disabled>Analyze</button>
4242
<button id="btn-columns" class="toolbar-btn" title="Show/hide columns" disabled>Columns</button>
43+
<button id="btn-word-wrap" class="toolbar-btn" title="Toggle word wrap (⌥Z)">Wrap</button>
4344
</div>
4445
<div class="toolbar-right">
4546
<button id="btn-help" class="toolbar-btn small" title="Keyboard shortcuts & help">?</button>

src/renderer/renderer.ts

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ const ZOOM_MAX = 200;
218218
const ZOOM_STEP = 10;
219219
let zoomLevel = 100; // Percentage (100 = default)
220220

221+
// Word wrap setting
222+
let wordWrapEnabled = false;
223+
221224
// Get current line height based on zoom
222225
function getLineHeight(): number {
223226
return Math.round(BASE_LINE_HEIGHT * (zoomLevel / 100));
@@ -294,6 +297,7 @@ const elements = {
294297
btnCancelSplit: document.getElementById('btn-cancel-split') as HTMLButtonElement,
295298
// Columns
296299
btnColumns: document.getElementById('btn-columns') as HTMLButtonElement,
300+
btnWordWrap: document.getElementById('btn-word-wrap') as HTMLButtonElement,
297301
columnsModal: document.getElementById('columns-modal') as HTMLDivElement,
298302
columnsLoading: document.getElementById('columns-loading') as HTMLDivElement,
299303
columnsContent: document.getElementById('columns-content') as HTMLDivElement,
@@ -845,7 +849,12 @@ function renderVisibleLines(): void {
845849

846850
// Use transform for GPU-accelerated positioning (smoother scrolling)
847851
// Note: no right:0 to allow horizontal scroll expansion
848-
lineElement.style.cssText = `position:absolute;top:0;left:0;transform:translateY(${top}px);will-change:transform;white-space:pre;`;
852+
if (wordWrapEnabled) {
853+
// Word wrap mode: use relative positioning for natural flow
854+
lineElement.style.cssText = `position:relative;white-space:pre-wrap;word-break:break-all;`;
855+
} else {
856+
lineElement.style.cssText = `position:absolute;top:0;left:0;transform:translateY(${top}px);will-change:transform;white-space:pre;`;
857+
}
849858

850859
fragment.appendChild(lineElement);
851860

@@ -863,8 +872,14 @@ function renderVisibleLines(): void {
863872
logContentElement.appendChild(fragment);
864873

865874
// Set content size for scrolling
866-
logContentElement.style.height = `${virtualHeight}px`;
867-
logContentElement.style.minWidth = `${Math.max(maxContentWidth, logViewerElement.clientWidth)}px`;
875+
if (wordWrapEnabled) {
876+
// Word wrap mode: let content flow naturally
877+
logContentElement.style.height = 'auto';
878+
logContentElement.style.minWidth = '';
879+
} else {
880+
logContentElement.style.height = `${virtualHeight}px`;
881+
logContentElement.style.minWidth = `${Math.max(maxContentWidth, logViewerElement.clientWidth)}px`;
882+
}
868883

869884
// Restore horizontal scroll position
870885
if (scrollLeft > 0) {
@@ -1464,17 +1479,23 @@ async function buildMinimap(onProgress?: (percent: number) => void): Promise<voi
14641479
function renderMinimap(): void {
14651480
if (!minimapContentElement || !minimapElement) return;
14661481

1482+
const totalLines = getTotalLines();
1483+
if (totalLines === 0 || minimapData.length === 0) return;
1484+
14671485
const minimapHeight = minimapElement.clientHeight;
1468-
const lineHeight = Math.max(1, minimapHeight / minimapData.length);
1486+
// Calculate how many actual lines each sample represents
1487+
const linesPerSample = totalLines / minimapData.length;
1488+
// Each minimap line should take proportional height
1489+
const lineHeight = minimapHeight / minimapData.length;
14691490

14701491
minimapContentElement.innerHTML = '';
14711492

14721493
for (let i = 0; i < minimapData.length; i++) {
14731494
const data = minimapData[i];
14741495
const line = document.createElement('div');
14751496
line.className = `minimap-line level-${data.level || 'default'}`;
1497+
// Use exact height without margin to ensure proper alignment with markers
14761498
line.style.height = `${Math.max(1, lineHeight)}px`;
1477-
line.style.marginBottom = lineHeight < 2 ? '0' : '1px';
14781499
minimapContentElement.appendChild(line);
14791500
}
14801501

@@ -3525,6 +3546,27 @@ function resetZoom(): void {
35253546
applyZoom();
35263547
}
35273548

3549+
function toggleWordWrap(): void {
3550+
wordWrapEnabled = !wordWrapEnabled;
3551+
3552+
// Update button state
3553+
if (wordWrapEnabled) {
3554+
elements.btnWordWrap.classList.add('active');
3555+
} else {
3556+
elements.btnWordWrap.classList.remove('active');
3557+
}
3558+
3559+
if (logViewerElement) {
3560+
if (wordWrapEnabled) {
3561+
logViewerElement.classList.add('word-wrap');
3562+
} else {
3563+
logViewerElement.classList.remove('word-wrap');
3564+
}
3565+
// Re-render to apply word wrap
3566+
renderVisibleLines();
3567+
}
3568+
}
3569+
35283570
function applyZoom(): void {
35293571
// Update status bar
35303572
elements.statusZoom.textContent = `${zoomLevel}%`;
@@ -3768,6 +3810,86 @@ function setupKeyboardShortcuts(): void {
37683810
resetZoom();
37693811
}
37703812

3813+
// Arrow key navigation (only when not in input fields)
3814+
const isInputFocused = document.activeElement instanceof HTMLInputElement ||
3815+
document.activeElement instanceof HTMLTextAreaElement ||
3816+
document.activeElement?.closest('.terminal-container');
3817+
3818+
if (!isInputFocused && logViewerElement) {
3819+
const totalLines = getTotalLines();
3820+
const visibleLines = Math.floor(logViewerElement.clientHeight / getLineHeight());
3821+
3822+
// Arrow Down: Move down one line
3823+
if (e.key === 'ArrowDown') {
3824+
e.preventDefault();
3825+
const newLine = Math.min((state.selectedLine ?? 0) + 1, totalLines - 1);
3826+
state.selectedLine = newLine;
3827+
goToLine(newLine);
3828+
renderVisibleLines();
3829+
}
3830+
3831+
// Arrow Up: Move up one line
3832+
if (e.key === 'ArrowUp') {
3833+
e.preventDefault();
3834+
const newLine = Math.max((state.selectedLine ?? 0) - 1, 0);
3835+
state.selectedLine = newLine;
3836+
goToLine(newLine);
3837+
renderVisibleLines();
3838+
}
3839+
3840+
// Arrow Right: Scroll right
3841+
if (e.key === 'ArrowRight') {
3842+
e.preventDefault();
3843+
logViewerElement.scrollLeft += 50;
3844+
}
3845+
3846+
// Arrow Left: Scroll left
3847+
if (e.key === 'ArrowLeft') {
3848+
e.preventDefault();
3849+
logViewerElement.scrollLeft = Math.max(0, logViewerElement.scrollLeft - 50);
3850+
}
3851+
3852+
// Page Down: Move down by visible lines
3853+
if (e.key === 'PageDown' && !e.ctrlKey && !e.metaKey) {
3854+
e.preventDefault();
3855+
const newLine = Math.min((state.selectedLine ?? 0) + visibleLines, totalLines - 1);
3856+
state.selectedLine = newLine;
3857+
goToLine(newLine);
3858+
renderVisibleLines();
3859+
}
3860+
3861+
// Page Up: Move up by visible lines
3862+
if (e.key === 'PageUp' && !e.ctrlKey && !e.metaKey) {
3863+
e.preventDefault();
3864+
const newLine = Math.max((state.selectedLine ?? 0) - visibleLines, 0);
3865+
state.selectedLine = newLine;
3866+
goToLine(newLine);
3867+
renderVisibleLines();
3868+
}
3869+
3870+
// Home: Go to first line
3871+
if (e.key === 'Home' && !e.ctrlKey && !e.metaKey) {
3872+
e.preventDefault();
3873+
state.selectedLine = 0;
3874+
goToLine(0);
3875+
renderVisibleLines();
3876+
}
3877+
3878+
// End: Go to last line
3879+
if (e.key === 'End' && !e.ctrlKey && !e.metaKey) {
3880+
e.preventDefault();
3881+
state.selectedLine = totalLines - 1;
3882+
goToLine(totalLines - 1);
3883+
renderVisibleLines();
3884+
}
3885+
}
3886+
3887+
// Alt/Option + Z: Toggle word wrap (use code for Mac compatibility)
3888+
if (e.altKey && e.code === 'KeyZ') {
3889+
e.preventDefault();
3890+
toggleWordWrap();
3891+
}
3892+
37713893
// Help: F1 or ?
37723894
if (e.key === 'F1' || (e.key === '?' && !e.ctrlKey && !e.metaKey)) {
37733895
e.preventDefault();
@@ -3922,6 +4044,9 @@ function init(): void {
39224044
elements.btnColumnsAll.addEventListener('click', () => setAllColumnsVisibility(true));
39234045
elements.btnColumnsNone.addEventListener('click', () => setAllColumnsVisibility(false));
39244046

4047+
// Word wrap
4048+
elements.btnWordWrap.addEventListener('click', toggleWordWrap);
4049+
39254050
// Split mode and value change handlers
39264051
document.querySelectorAll('input[name="split-mode"]').forEach((radio) => {
39274052
radio.addEventListener('change', updateSplitPreview);

src/renderer/styles.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,14 @@ body {
948948
-webkit-tap-highlight-color: transparent;
949949
}
950950

951+
/* Word wrap mode */
952+
.virtual-log-viewer.word-wrap .log-line {
953+
white-space: pre-wrap;
954+
word-break: break-all;
955+
height: auto;
956+
min-height: var(--line-height, 20px);
957+
}
958+
951959
.log-line:hover {
952960
background-color: var(--bg-hover);
953961
}

0 commit comments

Comments
 (0)