diff --git a/src/renderer/index.html b/src/renderer/index.html index 29bcbd7..c9ef5be 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -145,6 +145,9 @@ +
diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index f505064..c9b420e 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -340,6 +340,7 @@ interface AppState { // "Mute" patterns: rows whose text contains any of these are dimmed IN PLACE // (kept for context, not removed) via a render-time class. Persisted globally. mutePatterns: string[]; + mutedSize: 'normal' | 'small' | 'tiny'; // opt-in smaller font for muted lines (mute menu) // Traceback tracebackResult: any | null; tracebackSort: 'time' | 'score'; @@ -419,6 +420,7 @@ const state: AppState = { hiddenLevels: new Set(JSON.parse(localStorage.getItem('logan-hidden-levels') || '[]')), levelBarFilterActive: false, mutePatterns: JSON.parse(localStorage.getItem('logan-mute-patterns') || '[]'), + mutedSize: (localStorage.getItem('logan-muted-size') as 'normal' | 'small' | 'tiny') || 'normal', tracebackResult: null, tracebackSort: 'time' as 'time' | 'score', tracebackFilterCat: 'all', @@ -2876,6 +2878,18 @@ function clearMutePatterns(): void { renderVisibleLines(); } +// Opt-in smaller font for muted lines (default 'normal' = as is). Applied via a body class +// so it works for already- and later-rendered rows without a re-render. Persisted. +function applyMutedSize(): void { + document.body.classList.toggle('muted-size-small', state.mutedSize === 'small'); + document.body.classList.toggle('muted-size-tiny', state.mutedSize === 'tiny'); +} +function setMutedSize(size: 'normal' | 'small' | 'tiny'): void { + state.mutedSize = size; + localStorage.setItem('logan-muted-size', size); + applyMutedSize(); +} + function createLineElementPooled(line: LogLine): HTMLDivElement { const div = lineElementPool.acquire(); div.dataset.lineNumber = String(line.lineNumber); @@ -4740,6 +4754,12 @@ function handleContextMenu(event: MouseEvent): void { menu.remove(); }); menu.appendChild(clearMutes); + // Muted line SIZE — opt-in smaller font so noise recedes (default As is). ✓ marks current. + for (const [label, val] of [['As is', 'normal'], ['Small', 'small'], ['Tiny', 'tiny']] as const) { + const it = menuItem(state.mutedSize === val ? '✓' : ' ', `Muted size: ${label}`); + it.addEventListener('click', () => { setMutedSize(val); menu.remove(); }); + menu.appendChild(it); + } menu.appendChild(menuSeparator()); } @@ -5689,6 +5709,44 @@ function findSubfolder(entries: LocalFolderFile[], subdirPath: string): LocalFol return null; } +// Reveal the active file in the folder tree: expand the folder + the ancestor subdirs on +// its path, re-render, then scroll it into view + flash it. The active row already carries +// the `.active` class (see renderFolderEntries); this just makes it visible + findable. +function revealActiveFileInTree(): void { + if (!state.filePath) { showToast('No active file to reveal'); return; } + const target = state.filePath; + let matched = false; + for (const folder of state.folders) { + const base = folder.path.replace(/[\\/]+$/, ''); + if (target !== base && !target.startsWith(base + '/') && !target.startsWith(base + '\\')) continue; + matched = true; + folder.collapsed = false; + // Un-collapse each ancestor directory of the file (those present in the loaded tree). + const rel = target.slice(base.length).replace(/^[\\/]+/, ''); + const parts = rel.split(/[\\/]/); + parts.pop(); // drop the filename + let acc = base; + for (const part of parts) { + acc = acc + '/' + part; + const sub = findSubfolder(folder.files, acc) || findSubfolder(folder.files, acc.replace(/\//g, '\\')); + if (sub) sub.collapsed = false; + } + break; + } + if (!matched) { showToast('Active file isn’t under a folder in the tree — add its folder first'); return; } + renderFolderTree(); + requestAnimationFrame(() => { + const el = elements.foldersList.querySelector('.folder-file.active') as HTMLElement | null; + if (el) { + el.scrollIntoView({ block: 'center', behavior: 'smooth' }); + el.classList.add('reveal-flash'); + setTimeout(() => el.classList.remove('reveal-flash'), 1400); + } else { + showToast('Located its folder, but the file row isn’t loaded yet — expand that folder'); + } + }); +} + function renderFolderTree(): void { if (state.folders.length === 0) { elements.foldersList.innerHTML = '

No folders open

'; @@ -22586,6 +22644,7 @@ function setupHelpTooltips(): void { function init(): void { // Detect platform and setup window controls setupWindowControls(); + applyMutedSize(); // restore the saved muted-line size (default: as is) // Load user settings from localStorage loadSettings(); @@ -23050,6 +23109,7 @@ function init(): void { elements.btnLiveSshRefresh.addEventListener('click', () => refreshSshHosts()); elements.btnLiveSshManage.addEventListener('click', () => showSshProfileManager()); elements.btnOpenSshFolder.addEventListener('click', () => openSshFolder()); + document.getElementById('btn-reveal-active-file')?.addEventListener('click', () => revealActiveFileInTree()); // Unified live connection event listeners (register once) setupLiveEventListeners(); diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 734b27b..3e8ad89 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -1550,6 +1550,12 @@ body.platform-darwin .titlebar { background: var(--accent-color); color: white; } +/* Brief flash when "Reveal active file" scrolls to it, so the eye catches it. */ +.folder-file.reveal-flash { animation: folder-reveal-flash 1.3s ease-out; } +@keyframes folder-reveal-flash { + 0%, 30% { box-shadow: inset 0 0 0 2px var(--accent-color, #5b8def); } + 100% { box-shadow: inset 0 0 0 2px transparent; } +} .folder-file-icon { width: 14px; @@ -2643,17 +2649,19 @@ button.is-cancelable:hover, .btn.is-cancelable:hover { filter: brightness(1.12); to bring it back to full opacity so it's still readable on demand. */ .log-line.muted { opacity: 0.28; - /* Smaller text so muted "noise" lines read as lighter/shorter. The ROW height stays - var(--line-height) — the virtual scroller needs a uniform per-line height — so this - shrinks the text within the row, not the row itself. Full size + opacity on hover. */ - font-size: 0.78em; transition: opacity 0.12s ease, font-size 0.1s ease; } -.log-line.muted .line-number { font-size: 0.85em; } .log-line.muted:hover { opacity: 1; font-size: 1em; } +/* Opt-in muted-line SIZE (mute menu → "Muted size"). Default = As is (no class on body). + Shrinks the text within the fixed 20px row (the virtual scroller needs a uniform row + height, so this does NOT reduce the row height). Full size on hover regardless. */ +.muted-size-small .log-line.muted { font-size: 0.82em; } +.muted-size-small .log-line.muted .line-number { font-size: 0.88em; } +.muted-size-tiny .log-line.muted { font-size: 0.66em; } +.muted-size-tiny .log-line.muted .line-number { font-size: 0.78em; } .line-number { min-width: 60px;