diff --git a/src/ui/file-explorer-badge.ts b/src/ui/file-explorer-badge.ts index 725c376..9f30bfc 100644 --- a/src/ui/file-explorer-badge.ts +++ b/src/ui/file-explorer-badge.ts @@ -18,17 +18,14 @@ export function installFileExplorerBadge( applyBadges(); }); - // Re-apply badges periodically - const interval = window.setInterval(() => { - applyBadges(); - }, 3000); - - plugin.register(() => { - window.clearInterval(interval); - }); + // Re-apply badges when file explorer updates (on file-open events) + plugin.registerEvent( + plugin.app.workspace.on('file-open', () => { + applyBadges(); + }) + ); return () => { - window.clearInterval(interval); removeBadges(); }; } diff --git a/src/ui/secret-block-widget.ts b/src/ui/secret-block-widget.ts deleted file mode 100644 index 84825c2..0000000 --- a/src/ui/secret-block-widget.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * CodeMirror ViewPlugin that renders ```ocke-v1 blocks as visual "secret" widgets. - * - * Key principle: the actual document text is NEVER modified. - * The encrypted data stays on disk and in the editor buffer at all times. - * This plugin only provides a visual overlay (widget decoration) that shows - * the decrypted content when the key is available, or a "locked" indicator otherwise. - */ - -import { - ViewPlugin, - ViewUpdate, - WidgetType, - Decoration, - DecorationSet, - EditorView, -} from '@codemirror/view'; -import { RangeSetBuilder } from '@codemirror/state'; -import type { CryptoEngine, EncryptionContext } from '../types'; -import { decodeInlineBlock } from '../format/inline-codec'; -import { parse } from '../format/parser'; -import { FORMAT_VERSION } from '../constants'; - -/** - * Regex to find ```ocke-v1 blocks with their positions. - */ -const ENCRYPTED_BLOCK_REGEX = /```ocke-v1\n([\s\S]*?)\n```/g; - -/** - * Cache of decrypted content keyed by base64 content. - */ -const decryptionCache = new Map(); - -/** - * Widget that displays decrypted content inside a styled container. - */ -class DecryptedBlockWidget extends WidgetType { - constructor( - private readonly plaintext: string | null, - private readonly isLocked: boolean, - private readonly encryptedPreview: string - ) { - super(); - } - - toDOM(): HTMLElement { - const container = document.createElement('div'); - container.className = 'ocke-secret-block'; - container.style.border = '1px solid var(--background-modifier-border)'; - container.style.borderRadius = '4px'; - container.style.margin = '4px 0'; - container.style.overflow = 'hidden'; - - // Header - const header = document.createElement('div'); - header.style.padding = '4px 8px'; - header.style.fontSize = '0.8em'; - header.style.fontWeight = 'bold'; - header.style.display = 'flex'; - header.style.alignItems = 'center'; - header.style.gap = '4px'; - - if (this.isLocked) { - header.style.background = 'var(--background-modifier-error)'; - header.style.color = 'var(--text-error)'; - header.textContent = '🔒 secret (locked — no key access)'; - } else if (this.plaintext === null) { - header.style.background = 'var(--background-modifier-border)'; - header.style.color = 'var(--text-muted)'; - header.textContent = '⏳ secret (decrypting...)'; - } else { - header.style.background = 'var(--interactive-accent)'; - header.style.color = 'var(--text-on-accent)'; - header.textContent = '🔓 secret (decrypted)'; - } - - container.appendChild(header); - - // Content - const content = document.createElement('pre'); - content.style.padding = '8px'; - content.style.margin = '0'; - content.style.whiteSpace = 'pre-wrap'; - content.style.wordBreak = 'break-word'; - content.style.fontFamily = 'var(--font-monospace)'; - content.style.fontSize = '0.9em'; - content.style.background = 'var(--background-secondary)'; - - if (this.isLocked) { - const preview = this.encryptedPreview.length > 80 - ? this.encryptedPreview.slice(0, 80) + '…' - : this.encryptedPreview; - content.textContent = preview; - content.style.color = 'var(--text-muted)'; - content.style.fontStyle = 'italic'; - } else if (this.plaintext === null) { - content.textContent = '...'; - content.style.color = 'var(--text-muted)'; - } else { - content.textContent = this.plaintext; - } - - container.appendChild(content); - return container; - } - - eq(other: DecryptedBlockWidget): boolean { - return ( - this.plaintext === other.plaintext && - this.isLocked === other.isLocked && - this.encryptedPreview === other.encryptedPreview - ); - } - - ignoreEvent(): boolean { - return false; - } -} - -/** - * Create the secret block ViewPlugin for a given crypto engine and vault context. - */ -export function createSecretBlockViewPlugin( - cryptoEngine: CryptoEngine, - getVaultName: () => string, - getFilePath: () => string -) { - return ViewPlugin.fromClass( - class { - decorations: DecorationSet; - - constructor(view: EditorView) { - this.decorations = this.buildDecorations(view); - } - - update(update: ViewUpdate) { - if (update.docChanged || update.viewportChanged || update.selectionSet) { - this.decorations = this.buildDecorations(update.view); - } - } - - buildDecorations(view: EditorView): DecorationSet { - const builder = new RangeSetBuilder(); - const doc = view.state.doc; - const text = doc.toString(); - - ENCRYPTED_BLOCK_REGEX.lastIndex = 0; - let match: RegExpExecArray | null; - - const blocks: Array<{ from: number; to: number; base64: string }> = []; - - while ((match = ENCRYPTED_BLOCK_REGEX.exec(text)) !== null) { - blocks.push({ - from: match.index, - to: match.index + match[0].length, - base64: match[1].trim(), - }); - } - - const cursorPos = view.state.selection.main.head; - - for (const block of blocks) { - // If cursor is inside this block, don't decorate — let user see raw text - if (cursorPos >= block.from && cursorPos <= block.to) { - continue; - } - - const cached = decryptionCache.get(block.base64); - - if (cached) { - const widget = new DecryptedBlockWidget( - cached.plaintext, - cached.error, - block.base64 - ); - builder.add(block.from, block.to, Decoration.replace({ widget })); - } else { - // Show placeholder and trigger async decryption - const widget = new DecryptedBlockWidget(null, false, block.base64); - builder.add(block.from, block.to, Decoration.replace({ widget })); - - // Trigger decryption without blocking - this.triggerDecrypt(block.base64, view); - } - } - - return builder.finish(); - } - - triggerDecrypt(base64Content: string, view: EditorView) { - // Don't re-trigger if already in cache or pending - if (decryptionCache.has(base64Content)) return; - - // Mark as pending to avoid duplicate requests - decryptionCache.set(base64Content, { plaintext: null, error: false }); - - const fullBlock = '```ocke-v1\n' + base64Content + '\n```'; - - (async () => { - try { - const binaryData = decodeInlineBlock(fullBlock); - if (!binaryData) { - decryptionCache.set(base64Content, { plaintext: null, error: true }); - return; - } - - const record = parse(binaryData); - const context: EncryptionContext = { - vaultName: getVaultName(), - filePath: getFilePath(), - formatVersion: FORMAT_VERSION, - }; - - const plaintextBytes = await cryptoEngine.decrypt(record, context); - const plaintext = new TextDecoder().decode(plaintextBytes); - decryptionCache.set(base64Content, { plaintext, error: false }); - } catch { - decryptionCache.set(base64Content, { plaintext: null, error: true }); - } - - // Request a re-render by scheduling a view measure - // This is safe and won't cause infinite loops - requestAnimationFrame(() => { - view.requestMeasure(); - }); - })(); - } - }, - { - decorations: (v) => v.decorations, - } - ); -} - -/** - * Clear the decryption cache (e.g., on plugin unload). - */ -export function clearDecryptionCache(): void { - decryptionCache.clear(); -} diff --git a/src/ui/settings-tab.ts b/src/ui/settings-tab.ts index 681b493..2078166 100644 --- a/src/ui/settings-tab.ts +++ b/src/ui/settings-tab.ts @@ -55,10 +55,8 @@ export class CloudKmsSettingsTab extends PluginSettingTab { const { containerEl } = this; containerEl.empty(); - new Setting(containerEl).setName("Cloud KMS Encryption Settings").setHeading(); - // --- Keys section --- - new Setting(containerEl).setName("Encryption Keys").setHeading(); + new Setting(containerEl).setName("Keys").setHeading(); containerEl.createEl("p", { text: "Add KMS keys with aliases. Use aliases in %%secret-start:alias%% markers. The default key is used when no alias is specified.", cls: "setting-item-description", @@ -68,7 +66,7 @@ export class CloudKmsSettingsTab extends PluginSettingTab { this.addDefaultKeySetting(containerEl); // --- Legacy --- - new Setting(containerEl).setName("Legacy Settings").setHeading(); + new Setting(containerEl).setName("Legacy").setHeading(); containerEl.createEl("p", { text: "Used as fallback if no keys are configured above.", cls: "setting-item-description",