import { ItemView, WorkspaceLeaf } from 'obsidian'; import { LogfireEvent, EventCategory } from '../types'; import { EventBus } from '../core/event-bus'; export const EVENT_STREAM_VIEW_TYPE = 'logfire-event-stream'; const MAX_VISIBLE_ENTRIES = 500; export class EventStreamView extends ItemView { private entries: LogfireEvent[] = []; private unsubscribe: (() => void) | null = null; private listEl!: HTMLElement; private viewPaused = false; private filterText = ''; private enabledCategories = new Set([ 'file', 'content', 'navigation', 'editor', 'vault', 'plugin', 'system', ]); constructor(leaf: WorkspaceLeaf, private eventBus: EventBus) { super(leaf); } getViewType(): string { return EVENT_STREAM_VIEW_TYPE; } getDisplayText(): string { return 'Logfire Events'; } getIcon(): string { return 'activity'; } async onOpen(): Promise { const container = this.containerEl.children[1] as HTMLElement; container.empty(); container.addClass('logfire-event-stream'); // Toolbar const toolbar = container.createDiv({ cls: 'logfire-stream-toolbar' }); const searchInput = toolbar.createEl('input', { type: 'text', placeholder: 'Filter by source...', cls: 'logfire-stream-search', }); searchInput.addEventListener('input', () => { this.filterText = searchInput.value.toLowerCase(); this.renderList(); }); const btnGroup = toolbar.createDiv({ cls: 'logfire-stream-btn-group' }); const pauseBtn = btnGroup.createEl('button', { text: 'Pause' }); pauseBtn.addEventListener('click', () => { this.viewPaused = !this.viewPaused; pauseBtn.textContent = this.viewPaused ? 'Resume' : 'Pause'; pauseBtn.toggleClass('is-active', this.viewPaused); }); const clearBtn = btnGroup.createEl('button', { text: 'Clear' }); clearBtn.addEventListener('click', () => { this.entries = []; this.renderList(); }); // Category toggles const categoryBar = container.createDiv({ cls: 'logfire-stream-categories' }); const categories: EventCategory[] = ['file', 'content', 'navigation', 'editor', 'vault', 'plugin', 'system']; for (const cat of categories) { const label = categoryBar.createEl('label', { cls: 'logfire-cat-toggle' }); const cb = label.createEl('input', { type: 'checkbox' }) as HTMLInputElement; cb.checked = true; label.createSpan({ text: cat }); cb.addEventListener('change', () => { if (cb.checked) { this.enabledCategories.add(cat); } else { this.enabledCategories.delete(cat); } this.renderList(); }); } // Event list this.listEl = container.createDiv({ cls: 'logfire-stream-list' }); // Subscribe to events this.unsubscribe = this.eventBus.onEvent('*', (event) => { if (this.viewPaused) return; this.entries.unshift(event); if (this.entries.length > MAX_VISIBLE_ENTRIES) { this.entries.length = MAX_VISIBLE_ENTRIES; } this.renderList(); }); } async onClose(): Promise { this.unsubscribe?.(); this.unsubscribe = null; } private renderList(): void { this.listEl.empty(); const filtered = this.entries.filter(e => { if (!this.enabledCategories.has(e.category)) return false; if (this.filterText && !e.source.toLowerCase().includes(this.filterText)) return false; return true; }); for (const event of filtered.slice(0, MAX_VISIBLE_ENTRIES)) { const row = this.listEl.createDiv({ cls: 'logfire-stream-row' }); const time = new Date(event.timestamp); const timeStr = `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}:${String(time.getSeconds()).padStart(2, '0')}`; const payloadSummary = this.summarizePayload(event); row.createSpan({ text: timeStr, cls: 'logfire-stream-time' }); row.createSpan({ text: event.type, cls: 'logfire-stream-type' }); row.createSpan({ text: event.source || '', cls: 'logfire-stream-source' }); if (payloadSummary) { row.createSpan({ text: payloadSummary, cls: 'logfire-stream-payload' }); } } } private summarizePayload(event: LogfireEvent): string { const p = event.payload; switch (event.type) { case 'content:words-changed': return `+${p.wordsAdded ?? 0}/-${p.wordsRemoved ?? 0} words`; case 'editor:change': return `+${p.insertedChars ?? 0}/-${p.deletedChars ?? 0} chars`; case 'nav:file-close': return typeof p.duration === 'number' ? `${Math.round(p.duration / 1000)}s` : ''; case 'file:rename': return `→ ${p.newName ?? ''}`; case 'file:move': return `→ ${p.newFolder ?? ''}`; case 'plugin:command-executed': return String(p.commandName ?? ''); default: return ''; } } }