From 27336783f769d5dd6114c9cfb1dc9baedb833ccb Mon Sep 17 00:00:00 2001 From: Conway Date: Mon, 6 Apr 2026 18:40:40 -0400 Subject: v1 --- .obsidian/plugins/abacus-sorter/main.js | 1077 ++++++++++++++++++++++ .obsidian/plugins/abacus-sorter/manifest.json | 9 + .obsidian/plugins/abacus-sorter/styles.css | 139 +++ .obsidian/themes/Spectroscope-Gruv/manifest.json | 9 + .obsidian/themes/Spectroscope-Gruv/theme.css | 473 ++++++++++ .obsidian/themes/Spectroscope-Noir/manifest.json | 9 + .obsidian/themes/Spectroscope-Noir/theme.css | 474 ++++++++++ 7 files changed, 2190 insertions(+) create mode 100644 .obsidian/plugins/abacus-sorter/main.js create mode 100644 .obsidian/plugins/abacus-sorter/manifest.json create mode 100644 .obsidian/plugins/abacus-sorter/styles.css create mode 100644 .obsidian/themes/Spectroscope-Gruv/manifest.json create mode 100644 .obsidian/themes/Spectroscope-Gruv/theme.css create mode 100644 .obsidian/themes/Spectroscope-Noir/manifest.json create mode 100644 .obsidian/themes/Spectroscope-Noir/theme.css (limited to '.obsidian') diff --git a/.obsidian/plugins/abacus-sorter/main.js b/.obsidian/plugins/abacus-sorter/main.js new file mode 100644 index 0000000..966e1a3 --- /dev/null +++ b/.obsidian/plugins/abacus-sorter/main.js @@ -0,0 +1,1077 @@ +/* +Abacus Custom Sorter. It's a config-driven custom file explorer sorter for Obsidian. +Inspired by SebastianMC's Custom File Explorer sorting. +This plugin however does not use a bookmark dependency. +*/ + +var obsidian = require('obsidian'); + +// Default Settings + +const DEFAULT_SETTINGS = { + suspended: false, + sortSpecFile: '', + statusBarEnabled: true, + notificationsEnabled: true, + contextMenuEnabled: true, + delayForInitialApplication: 0, + manualOrder: {} // { folderPath: [itemName, itemName, ...] } +}; + +// Sort Spec Parser + +const SORT_ORDERS = { + 'a-z': { key: 'alpha', reverse: false, caseSensitive: false }, + 'A-Z': { key: 'alpha', reverse: false, caseSensitive: true }, + 'z-a': { key: 'alpha', reverse: true, caseSensitive: false }, + 'Z-A': { key: 'alpha', reverse: true, caseSensitive: true }, + 'modified-new': { key: 'mtime', reverse: false }, + 'modified-old': { key: 'mtime', reverse: true }, + 'created-new': { key: 'ctime', reverse: false }, + 'created-old': { key: 'ctime', reverse: true }, + 'natural-a-z': { key: 'natural', reverse: false }, + 'natural-z-a': { key: 'natural', reverse: true }, + 'files-first': { key: 'type', filesFirst: true }, + 'folders-first': { key: 'type', filesFirst: false }, + 'by-metadata': { key: 'metadata' }, + 'manual': { key: 'manual' }, +}; + +const MATCH_TYPES = { + 'exact': 'exact', + 'prefix': 'prefix', + 'suffix': 'suffix', + 'regex': 'regex', + 'with-metadata': 'metadata', + 'files': 'files', + 'folders': 'folders', + 'all': 'all', +}; + +function naturalCompare(a, b) { + return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }); +} + +function parseSortSpec(text) { + if (!text || !text.trim()) return null; + const lines = text.split('\n'); + const specs = {}; + let currentFolder = null; + let currentGroups = []; + let currentGroup = null; + + function finishFolder() { + if (currentFolder !== null) { + if (currentGroup) currentGroups.push(currentGroup); + specs[currentFolder] = { groups: currentGroups }; + currentGroups = []; + currentGroup = null; + } + } + + for (let rawLine of lines) { + const line = rawLine.trim(); + if (!line || line.startsWith('//') || line.startsWith('#')) continue; + + // Folder target + if (line.startsWith('target-folder:')) { + finishFolder(); + currentFolder = line.slice('target-folder:'.length).trim(); + if (currentFolder === '/') currentFolder = '/'; + continue; + } + + // Wildcard folder target + if (line.startsWith('target-folder-wildcard:')) { + finishFolder(); + currentFolder = line.slice('target-folder-wildcard:'.length).trim(); + continue; + } + + if (currentFolder === null) continue; + + // Sort order line + if (line.startsWith('>') || line.startsWith('< ')) { + const orderStr = line.replace(/^[><]\s*/, '').trim(); + const parts = orderStr.split(/\s+/); + const orderName = parts[0]; + const order = SORT_ORDERS[orderName]; + if (order && currentGroup) { + currentGroup.order = { ...order }; + if (order.key === 'metadata' && parts.length > 1) { + currentGroup.order.metadataField = parts[1]; + if (parts.includes('numeric')) currentGroup.order.numeric = true; + } + } else if (order && currentGroups.length === 0 && !currentGroup) { + // Default order for the folder + if (!specs[currentFolder]) specs[currentFolder] = { groups: [] }; + specs[currentFolder].defaultOrder = { ...order }; + if (order.key === 'metadata' && parts.length > 1) { + specs[currentFolder].defaultOrder.metadataField = parts[1]; + if (parts.includes('numeric')) specs[currentFolder].defaultOrder.numeric = true; + } + } + continue; + } + + // Separator + if (line === '---' || line === '***') { + if (currentGroup) { + currentGroups.push(currentGroup); + currentGroup = null; + } + continue; + } + + // Match rule + let matched = false; + for (const [keyword, type] of Object.entries(MATCH_TYPES)) { + if (line.startsWith(keyword + ':') || line === keyword) { + if (currentGroup) currentGroups.push(currentGroup); + const value = line.includes(':') ? line.slice(keyword.length + 1).trim() : ''; + currentGroup = { matchType: type, matchValue: value, order: null }; + if (type === 'regex' && value) { + try { currentGroup.matchRegex = new RegExp(value); } catch(e) { /* skip invalid regex */ } + } + matched = true; + break; + } + } + if (matched) continue; + + // If nothing else matched, treat as a group label or exact match shorthand + if (currentGroup) currentGroups.push(currentGroup); + currentGroup = { matchType: 'exact', matchValue: line, order: null }; + } + + finishFolder(); + return Object.keys(specs).length > 0 ? specs : null; +} + +// Items from getSortedFolderItems may be FileExplorerItem (with .file) +// or raw TAbstractFile objects, depending on Obsidian version. This helper normalizes. +function getItemFile(item) { + return item && item.file ? item.file : item; +} + +function getItemName(item) { + const f = getItemFile(item); + return f ? (f.name || '') : ''; +} + +function itemMatchesGroup(item, group, app) { + const f = getItemFile(item); + const name = f ? (f.name || '') : ''; + const basename = f ? (f.basename || name.replace(/\.[^.]+$/, '')) : ''; + + switch (group.matchType) { + case 'exact': + return name === group.matchValue || basename === group.matchValue; + case 'prefix': + return name.startsWith(group.matchValue) || basename.startsWith(group.matchValue); + case 'suffix': + return name.endsWith(group.matchValue) || basename.endsWith(group.matchValue); + case 'regex': + return group.matchRegex ? group.matchRegex.test(name) || group.matchRegex.test(basename) : false; + case 'metadata': { + if (!f || !f.path) return false; + const cache = app.metadataCache.getCache(f.path); + if (!cache || !cache.frontmatter) return false; + return cache.frontmatter[group.matchValue] !== undefined; + } + case 'files': + return !!(f && f.children === undefined); + case 'folders': + return !!(f && f.children !== undefined); + case 'all': + return true; + default: + return false; + } +} + +// Sorting Engine + +function getMetadataValue(item, field, app) { + const f = getItemFile(item); + if (!f || !f.path) return null; + const cache = app.metadataCache.getCache(f.path); + if (!cache || !cache.frontmatter) return null; + return cache.frontmatter[field] ?? null; +} + +function makeComparator(order, app, manualOrderList) { + if (!order) return null; + + switch (order.key) { + case 'alpha': + return (a, b) => { + const an = getItemName(a); + const bn = getItemName(b); + const cmp = order.caseSensitive + ? an.localeCompare(bn) + : an.toLowerCase().localeCompare(bn.toLowerCase()); + return order.reverse ? -cmp : cmp; + }; + + case 'natural': + return (a, b) => { + const an = getItemName(a); + const bn = getItemName(b); + const cmp = naturalCompare(an, bn); + return order.reverse ? -cmp : cmp; + }; + + case 'mtime': + return (a, b) => { + const af = getItemFile(a); + const bf = getItemFile(b); + const at = af && af.stat ? af.stat.mtime : 0; + const bt = bf && bf.stat ? bf.stat.mtime : 0; + return order.reverse ? at - bt : bt - at; + }; + + case 'ctime': + return (a, b) => { + const af = getItemFile(a); + const bf = getItemFile(b); + const at = af && af.stat ? af.stat.ctime : 0; + const bt = bf && bf.stat ? bf.stat.ctime : 0; + return order.reverse ? at - bt : bt - at; + }; + + case 'type': + return (a, b) => { + const af = getItemFile(a); + const bf = getItemFile(b); + const aIsFolder = !!(af && af.children !== undefined); + const bIsFolder = !!(bf && bf.children !== undefined); + if (aIsFolder === bIsFolder) return 0; + if (order.filesFirst) return aIsFolder ? 1 : -1; + return aIsFolder ? -1 : 1; + }; + + case 'metadata': + return (a, b) => { + let av = getMetadataValue(a, order.metadataField, app); + let bv = getMetadataValue(b, order.metadataField, app); + if (av === null && bv === null) return 0; + if (av === null) return 1; + if (bv === null) return -1; + if (order.numeric) { + av = parseFloat(av); bv = parseFloat(bv); + if (isNaN(av)) return 1; + if (isNaN(bv)) return -1; + return av - bv; + } + return String(av).localeCompare(String(bv)); + }; + + case 'manual': + return (a, b) => { + if (!manualOrderList) return 0; + const an = getItemName(a); + const bn = getItemName(b); + const ai = manualOrderList.indexOf(an); + const bi = manualOrderList.indexOf(bn); + if (ai === -1 && bi === -1) return 0; + if (ai === -1) return 1; + if (bi === -1) return -1; + return ai - bi; + }; + + default: + return null; + } +} + +function sortItems(items, spec, app, manualOrderList) { + if (!spec || !spec.groups || spec.groups.length === 0) { + // No groups, just apply default order if present + if (spec && spec.defaultOrder) { + const cmp = makeComparator(spec.defaultOrder, app, manualOrderList); + if (cmp) items.sort(cmp); + } + return items; + } + + // Assign items to groups + const buckets = spec.groups.map(() => []); + const unmatched = []; + + for (const item of items) { + let placed = false; + for (let g = 0; g < spec.groups.length; g++) { + if (itemMatchesGroup(item, spec.groups[g], app)) { + buckets[g].push(item); + placed = true; + break; + } + } + if (!placed) unmatched.push(item); + } + + // Sort each bucket + const defaultCmp = spec.defaultOrder ? makeComparator(spec.defaultOrder, app, manualOrderList) : null; + for (let g = 0; g < spec.groups.length; g++) { + const group = spec.groups[g]; + const cmp = group.order ? makeComparator(group.order, app, manualOrderList) : defaultCmp; + if (cmp) buckets[g].sort(cmp); + } + if (defaultCmp) unmatched.sort(defaultCmp); + + // Reassemble + const result = []; + for (const bucket of buckets) result.push(...bucket); + result.push(...unmatched); + return result; +} + +// Manual Order Sorting (standalone, no spec)─ + +function applyManualOrder(items, manualOrderList) { + if (!manualOrderList || manualOrderList.length === 0) return items; + + const ordered = []; + const unordered = []; + + for (const item of items) { + const name = getItemName(item); + const idx = manualOrderList.indexOf(name); + if (idx !== -1) { + ordered[idx] = item; + } else { + unordered.push(item); + } + } + + // Filter out sparse gaps and append unordered + return ordered.filter(Boolean).concat(unordered); +} + +// File Explorer Patcher─ + +function getFileExplorerLeaf(app) { + const leaves = app.workspace.getLeavesOfType('file-explorer'); + return leaves.length > 0 ? leaves[0] : null; +} + +function getFileExplorer(app) { + const leaf = getFileExplorerLeaf(app); + return leaf ? leaf.view : null; +} + +/** + * Monkey-patch a single method on an object, preserving the original. + * Returns an uninstall function that restores the original. + */ +function monkeyPatch(obj, methodName, wrapper) { + const original = obj[methodName]; + const hasOwn = obj.hasOwnProperty(methodName); + + // The wrapper receives the original function as its first arg + obj[methodName] = function(...args) { + return wrapper.call(this, original.bind(this), ...args); + }; + + // Return uninstaller + return function uninstall() { + if (hasOwn) { + obj[methodName] = original; + } else { + delete obj[methodName]; + } + }; +} + +function patchFileExplorer(plugin) { + const explorer = getFileExplorer(plugin.app); + if (!explorer) { + console.warn('Abacus: File explorer view not found'); + return false; + } + + plugin.fileExplorer = explorer; + + // Verify the view has getSortedFolderItems and requestSort + if (typeof explorer.getSortedFolderItems !== 'function') { + console.warn('Abacus: getSortedFolderItems not found on file explorer view'); + return false; + } + if (typeof explorer.requestSort !== 'function') { + console.warn('Abacus: requestSort not found on file explorer view'); + } + + if (plugin.unpatchFn) return true; // already patched + + // Determine where to patch: prefer prototype, fall back to instance + const proto = explorer.constructor.prototype; + const target = (typeof proto.getSortedFolderItems === 'function') ? proto : explorer; + + console.log('Abacus: Patching getSortedFolderItems on', target === proto ? 'prototype' : 'instance'); + + // Patch getSortedFolderItems - this is called by Obsidian to get the ordered + // list of children (FileExplorerItem[]) for each folder in the file explorer + plugin.unpatchFn = monkeyPatch( + target, + 'getSortedFolderItems', + function(originalFn, folder) { + // Always call original first to get Obsidian's default sorted items + const defaultResult = originalFn(folder); + + if (plugin.settings.suspended) return defaultResult; + + const folderPath = folder ? folder.path : ''; + if (!defaultResult || defaultResult.length === 0) return defaultResult; + + // Check for sort spec + const spec = plugin.getSpecForFolder(folderPath); + const manualOrderList = plugin.settings.manualOrder[folderPath] || null; + + if (spec) { + return sortItems([...defaultResult], spec, plugin.app, manualOrderList); + } else if (manualOrderList && manualOrderList.length > 0) { + return applyManualOrder([...defaultResult], manualOrderList); + } + + return defaultResult; + } + ); + + console.log('Abacus: File explorer patched successfully'); + return true; +} + +function unpatchFileExplorer(plugin) { + if (plugin.unpatchFn) { + plugin.unpatchFn(); + plugin.unpatchFn = null; + } +} + +// Settings Tab─ + +class AbacusSettingTab extends obsidian.PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + + display() { + const { containerEl } = this; + containerEl.empty(); + + containerEl.createEl('h2', { text: 'Abacus Custom Sorter' }); + + new obsidian.Setting(containerEl) + .setName('Suspend sorting') + .setDesc('Temporarily disable custom sorting and revert to Obsidian defaults.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.suspended) + .onChange(async (value) => { + this.plugin.settings.suspended = value; + await this.plugin.saveSettings(); + this.plugin.refreshExplorer(); + this.plugin.updateStatusBar(); + })); + + new obsidian.Setting(containerEl) + .setName('Sort specification file') + .setDesc('Path to a sort spec file (relative to vault root). Leave empty for none. Example: .obsidian/sort-spec.md') + .addText(text => text + .setPlaceholder('.obsidian/sort-spec.md') + .setValue(this.plugin.settings.sortSpecFile) + .onChange(async (value) => { + this.plugin.settings.sortSpecFile = value.trim(); + await this.plugin.saveSettings(); + await this.plugin.loadSortSpec(); + this.plugin.refreshExplorer(); + })); + + new obsidian.Setting(containerEl) + .setName('Status bar') + .setDesc('Show sort status in the status bar.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.statusBarEnabled) + .onChange(async (value) => { + this.plugin.settings.statusBarEnabled = value; + await this.plugin.saveSettings(); + this.plugin.updateStatusBar(); + })); + + new obsidian.Setting(containerEl) + .setName('Notifications') + .setDesc('Show notices when sorting state changes.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.notificationsEnabled) + .onChange(async (value) => { + this.plugin.settings.notificationsEnabled = value; + await this.plugin.saveSettings(); + })); + + new obsidian.Setting(containerEl) + .setName('Context menu') + .setDesc('Show sorting options in file explorer context menu.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.contextMenuEnabled) + .onChange(async (value) => { + this.plugin.settings.contextMenuEnabled = value; + await this.plugin.saveSettings(); + })); + + new obsidian.Setting(containerEl) + .setName('Initial delay (ms)') + .setDesc('Delay before applying sort on startup. Increase if sorting fails to apply.') + .addText(text => text + .setValue(String(this.plugin.settings.delayForInitialApplication)) + .onChange(async (value) => { + const num = parseInt(value, 10); + if (!isNaN(num) && num >= 0) { + this.plugin.settings.delayForInitialApplication = num; + await this.plugin.saveSettings(); + } + })); + + // Manual order management + containerEl.createEl('h3', { text: 'Manual Order' }); + + const manualKeys = Object.keys(this.plugin.settings.manualOrder); + if (manualKeys.length === 0) { + containerEl.createEl('p', { + text: 'No manual orders defined. Right-click a folder in the file explorer to set manual ordering.', + cls: 'setting-item-description' + }); + } else { + for (const folder of manualKeys.sort()) { + const count = this.plugin.settings.manualOrder[folder].length; + new obsidian.Setting(containerEl) + .setName(folder || '/ (vault root)') + .setDesc(`${count} item(s) ordered`) + .addButton(btn => btn + .setButtonText('Clear') + .setWarning() + .onClick(async () => { + delete this.plugin.settings.manualOrder[folder]; + await this.plugin.saveSettings(); + this.plugin.refreshExplorer(); + this.display(); // refresh settings view + })); + } + } + + // Sort spec viewer + if (this.plugin.sortSpecs) { + containerEl.createEl('h3', { text: 'Active Sort Spec' }); + const specFolders = Object.keys(this.plugin.sortSpecs); + containerEl.createEl('p', { + text: `${specFolders.length} folder rule(s) loaded from spec file.`, + cls: 'setting-item-description' + }); + for (const f of specFolders.sort()) { + const groupCount = this.plugin.sortSpecs[f].groups ? this.plugin.sortSpecs[f].groups.length : 0; + containerEl.createEl('p', { + text: ` ${f || '/'}: ${groupCount} group(s)`, + cls: 'setting-item-description' + }); + } + } + } +} + +// Manual Order Modal─ + +class ManualOrderModal extends obsidian.Modal { + constructor(app, plugin, folderPath, items) { + super(app); + this.plugin = plugin; + this.folderPath = folderPath; + this.items = items.map(i => getItemName(i)); + this.orderList = [...this.items]; + + // If there's an existing manual order, apply it + const existing = plugin.settings.manualOrder[folderPath]; + if (existing && existing.length > 0) { + const ordered = []; + const rest = []; + for (const name of existing) { + if (this.items.includes(name)) ordered.push(name); + } + for (const name of this.items) { + if (!ordered.includes(name)) rest.push(name); + } + this.orderList = ordered.concat(rest); + } + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('abacus-manual-order-modal'); + + contentEl.createEl('h2', { text: 'Manual Sort Order' }); + contentEl.createEl('p', { + text: `Folder: ${this.folderPath || '/ (vault root)'}`, + cls: 'abacus-modal-subtitle' + }); + contentEl.createEl('p', { + text: 'Drag items to reorder, or use the arrow buttons.', + cls: 'abacus-modal-hint' + }); + + this.listEl = contentEl.createEl('div', { cls: 'abacus-order-list' }); + this.renderList(); + + const btnRow = contentEl.createEl('div', { cls: 'abacus-modal-buttons' }); + + const saveBtn = btnRow.createEl('button', { text: 'Save Order', cls: 'mod-cta' }); + saveBtn.addEventListener('click', async () => { + this.plugin.settings.manualOrder[this.folderPath] = [...this.orderList]; + await this.plugin.saveSettings(); + this.plugin.refreshExplorer(); + if (this.plugin.settings.notificationsEnabled) { + new obsidian.Notice(`Abacus: Manual order saved for ${this.folderPath || '/'}`); + } + this.close(); + }); + + const clearBtn = btnRow.createEl('button', { text: 'Clear Order' }); + clearBtn.addEventListener('click', async () => { + delete this.plugin.settings.manualOrder[this.folderPath]; + await this.plugin.saveSettings(); + this.plugin.refreshExplorer(); + if (this.plugin.settings.notificationsEnabled) { + new obsidian.Notice(`Abacus: Manual order cleared for ${this.folderPath || '/'}`); + } + this.close(); + }); + + const cancelBtn = btnRow.createEl('button', { text: 'Cancel' }); + cancelBtn.addEventListener('click', () => this.close()); + } + + renderList() { + this.listEl.empty(); + for (let i = 0; i < this.orderList.length; i++) { + const row = this.listEl.createEl('div', { cls: 'abacus-order-row' }); + row.setAttribute('draggable', 'true'); + row.dataset.index = String(i); + + // Drag handle + const handle = row.createEl('span', { cls: 'abacus-drag-handle', text: '⠿' }); + + // Index number + row.createEl('span', { cls: 'abacus-order-index', text: String(i + 1) }); + + // Item name + row.createEl('span', { cls: 'abacus-order-name', text: this.orderList[i] }); + + // Up/down buttons + const btnContainer = row.createEl('span', { cls: 'abacus-order-buttons' }); + + if (i > 0) { + const upBtn = btnContainer.createEl('button', { cls: 'abacus-order-btn', text: '\u25B2' }); + upBtn.addEventListener('click', () => this.moveItem(i, i - 1)); + } + if (i < this.orderList.length - 1) { + const downBtn = btnContainer.createEl('button', { cls: 'abacus-order-btn', text: '\u25BC' }); + downBtn.addEventListener('click', () => this.moveItem(i, i + 1)); + } + + // Drag events + row.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('text/plain', String(i)); + row.addClass('abacus-dragging'); + }); + row.addEventListener('dragend', () => { + row.removeClass('abacus-dragging'); + }); + row.addEventListener('dragover', (e) => { + e.preventDefault(); + row.addClass('abacus-drag-over'); + }); + row.addEventListener('dragleave', () => { + row.removeClass('abacus-drag-over'); + }); + row.addEventListener('drop', (e) => { + e.preventDefault(); + row.removeClass('abacus-drag-over'); + const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10); + if (!isNaN(fromIndex) && fromIndex !== i) { + this.moveItem(fromIndex, i); + } + }); + } + } + + moveItem(from, to) { + const item = this.orderList.splice(from, 1)[0]; + this.orderList.splice(to, 0, item); + this.renderList(); + } + + onClose() { + this.contentEl.empty(); + } +} + +// Quick Sort Modal (apply a preset sort to a folder) + +class QuickSortModal extends obsidian.Modal { + constructor(app, plugin, folderPath, items) { + super(app); + this.plugin = plugin; + this.folderPath = folderPath; + this.items = items; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('abacus-quick-sort-modal'); + + contentEl.createEl('h2', { text: 'Quick Sort' }); + contentEl.createEl('p', { + text: `Apply a sort order to: ${this.folderPath || '/ (vault root)'}`, + cls: 'abacus-modal-subtitle' + }); + + const options = [ + ['A \u2192 Z (natural)', 'natural-a-z'], + ['Z \u2192 A (natural)', 'natural-z-a'], + ['A \u2192 Z (strict)', 'a-z'], + ['Z \u2192 A (strict)', 'z-a'], + ['Newest modified first', 'modified-new'], + ['Oldest modified first', 'modified-old'], + ['Newest created first', 'created-new'], + ['Oldest created first', 'created-old'], + ['Folders first', 'folders-first'], + ['Files first', 'files-first'], + ]; + + const list = contentEl.createEl('div', { cls: 'abacus-quick-sort-list' }); + + for (const [label, orderKey] of options) { + const btn = list.createEl('button', { text: label, cls: 'abacus-quick-sort-option' }); + btn.addEventListener('click', async () => { + const order = SORT_ORDERS[orderKey]; + if (order) { + const cmp = makeComparator(order, this.app, null); + if (cmp) { + const sorted = [...this.items].sort((a, b) => { + return cmp(a, b); + }); + const names = sorted.map(i => getItemName(i)); + this.plugin.settings.manualOrder[this.folderPath] = names; + await this.plugin.saveSettings(); + this.plugin.refreshExplorer(); + if (this.plugin.settings.notificationsEnabled) { + new obsidian.Notice(`Abacus: Sorted ${this.folderPath || '/'} by ${label}`); + } + } + } + this.close(); + }); + } + + const cancelBtn = contentEl.createEl('button', { text: 'Cancel', cls: 'abacus-quick-sort-cancel' }); + cancelBtn.addEventListener('click', () => this.close()); + } + + onClose() { + this.contentEl.empty(); + } +} + +// Main Plugin Class + +class AbacusSorterPlugin extends obsidian.Plugin { + async onload() { + await this.loadSettings(); + + this.sortSpecs = null; + this.originalSort = null; + this.fileExplorer = null; + + // Add settings tab + this.addSettingTab(new AbacusSettingTab(this.app, this)); + + // Ribbon icon + this.ribbonIconEl = this.addRibbonIcon('arrow-up-down', 'Abacus Custom Sorter', () => { + this.settings.suspended = !this.settings.suspended; + this.saveSettings(); + this.refreshExplorer(); + this.updateStatusBar(); + this.updateRibbonIcon(); + if (this.settings.notificationsEnabled) { + new obsidian.Notice(`Abacus: Sorting ${this.settings.suspended ? 'suspended' : 'active'}`); + } + }); + this.updateRibbonIcon(); + + // Status bar + this.statusBarEl = this.addStatusBarItem(); + this.updateStatusBar(); + + // Context menu + this.registerEvent( + this.app.workspace.on('file-menu', (menu, file, source) => { + if (!this.settings.contextMenuEnabled) return; + if (source !== 'file-explorer-context-menu') return; + + const folderPath = file.children !== undefined ? file.path : (file.parent ? file.parent.path : ''); + + menu.addSeparator(); + + menu.addItem((item) => { + item.setTitle('Abacus: Set manual order...') + .setIcon('arrow-up-down') + .onClick(() => { + this.openManualOrderModal(folderPath); + }); + }); + + menu.addItem((item) => { + item.setTitle('Abacus: Quick sort...') + .setIcon('sort-asc') + .onClick(() => { + this.openQuickSortModal(folderPath); + }); + }); + + if (this.settings.manualOrder[folderPath]) { + menu.addItem((item) => { + item.setTitle('Abacus: Clear manual order') + .setIcon('x') + .onClick(async () => { + delete this.settings.manualOrder[folderPath]; + await this.saveSettings(); + this.refreshExplorer(); + if (this.settings.notificationsEnabled) { + new obsidian.Notice(`Abacus: Manual order cleared for ${folderPath || '/'}`); + } + }); + }); + } + + menu.addItem((item) => { + item.setTitle(this.settings.suspended ? 'Abacus: Resume sorting' : 'Abacus: Suspend sorting') + .setIcon(this.settings.suspended ? 'play' : 'pause') + .onClick(async () => { + this.settings.suspended = !this.settings.suspended; + await this.saveSettings(); + this.refreshExplorer(); + this.updateStatusBar(); + this.updateRibbonIcon(); + }); + }); + }) + ); + + // Wait for layout ready, then patch + this.app.workspace.onLayoutReady(() => { + const delay = this.settings.delayForInitialApplication || 0; + if (delay > 0) { + setTimeout(() => this.initializeSorting(), delay); + } else { + this.initializeSorting(); + } + }); + + // Watch for file changes to refresh + this.registerEvent(this.app.vault.on('rename', () => this.debouncedRefresh())); + this.registerEvent(this.app.vault.on('create', () => this.debouncedRefresh())); + this.registerEvent(this.app.vault.on('delete', () => this.debouncedRefresh())); + + // Commands + this.addCommand({ + id: 'toggle-sorting', + name: 'Toggle custom sorting', + callback: () => { + this.settings.suspended = !this.settings.suspended; + this.saveSettings(); + this.refreshExplorer(); + this.updateStatusBar(); + this.updateRibbonIcon(); + if (this.settings.notificationsEnabled) { + new obsidian.Notice(`Abacus: Sorting ${this.settings.suspended ? 'suspended' : 'active'}`); + } + } + }); + + this.addCommand({ + id: 'reload-sort-spec', + name: 'Reload sort specification', + callback: async () => { + await this.loadSortSpec(); + this.refreshExplorer(); + new obsidian.Notice('Abacus: Sort spec reloaded'); + } + }); + } + + async onunload() { + unpatchFileExplorer(this); + // Trigger a re-sort so Obsidian reverts to its default order + const explorer = getFileExplorer(this.app); + if (explorer && typeof explorer.requestSort === 'function') { + explorer.requestSort(); + } + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + // Ensure manualOrder exists + if (!this.settings.manualOrder) this.settings.manualOrder = {}; + } + + async saveSettings() { + await this.saveData(this.settings); + } + + async initializeSorting() { + await this.loadSortSpec(); + const patched = patchFileExplorer(this); + if (patched) { + this.refreshExplorer(); + if (this.settings.notificationsEnabled) { + new obsidian.Notice('Abacus Custom Sorter: Active'); + } + } else { + // Retry once after a short delay + setTimeout(() => { + const retry = patchFileExplorer(this); + if (retry) { + this.refreshExplorer(); + if (this.settings.notificationsEnabled) { + new obsidian.Notice('Abacus Custom Sorter: Active'); + } + } + }, 1000); + } + } + + async loadSortSpec() { + this.sortSpecs = null; + const specPath = this.settings.sortSpecFile; + if (!specPath) return; + + try { + const file = this.app.vault.getAbstractFileByPath(specPath); + if (file && file instanceof obsidian.TFile) { + const content = await this.app.vault.cachedRead(file); + this.sortSpecs = parseSortSpec(content); + } + } catch (e) { + console.error('Abacus: Failed to load sort spec:', e); + } + } + + getSpecForFolder(folderPath) { + if (!this.sortSpecs) return null; + + // Exact match + if (this.sortSpecs[folderPath]) return this.sortSpecs[folderPath]; + + // Check wildcards: try parent paths with /* + const parts = folderPath.split('/'); + for (let i = parts.length - 1; i >= 0; i--) { + const partial = parts.slice(0, i).join('/') + '/*'; + if (this.sortSpecs[partial]) return this.sortSpecs[partial]; + } + + // Global wildcard + if (this.sortSpecs['/*']) return this.sortSpecs['/*']; + if (this.sortSpecs['*']) return this.sortSpecs['*']; + + return null; + } + + openManualOrderModal(folderPath) { + // Get the TFolder and its children directly from the vault + const folder = this.app.vault.getAbstractFileByPath(folderPath); + if (!folder || !folder.children) { + new obsidian.Notice('Abacus: Folder not found'); + return; + } + + // Build item wrappers that match what getSortedFolderItems returns + // Each needs a .file property pointing to the TAbstractFile + const items = folder.children.map(child => ({ file: child })); + + if (items.length === 0) { + new obsidian.Notice('Abacus: No items found in folder'); + return; + } + + // Sort by current display order (natural sort as baseline) + items.sort((a, b) => naturalCompare(getItemName(a), getItemName(b))); + + new ManualOrderModal(this.app, this, folderPath, items).open(); + } + + openQuickSortModal(folderPath) { + const folder = this.app.vault.getAbstractFileByPath(folderPath); + if (!folder || !folder.children) { + new obsidian.Notice('Abacus: Folder not found'); + return; + } + + const items = folder.children.map(child => ({ file: child })); + + if (items.length === 0) { + new obsidian.Notice('Abacus: No items found in folder'); + return; + } + + new QuickSortModal(this.app, this, folderPath, items).open(); + } + + refreshExplorer() { + // requestSort() tells the file explorer to re-call getSortedFolderItems + // for all visible folders, which triggers our patched version + const explorer = getFileExplorer(this.app); + if (explorer && typeof explorer.requestSort === 'function') { + explorer.requestSort(); + } + } + + _refreshTimeout = null; + debouncedRefresh() { + if (this._refreshTimeout) clearTimeout(this._refreshTimeout); + this._refreshTimeout = setTimeout(() => { + this.refreshExplorer(); + }, 300); + } + + updateStatusBar() { + if (!this.statusBarEl) return; + if (!this.settings.statusBarEnabled) { + this.statusBarEl.setText(''); + return; + } + const manualCount = Object.keys(this.settings.manualOrder).length; + const specCount = this.sortSpecs ? Object.keys(this.sortSpecs).length : 0; + const state = this.settings.suspended ? 'OFF' : 'ON'; + let text = `Abacus: ${state}`; + if (!this.settings.suspended && (manualCount > 0 || specCount > 0)) { + const parts = []; + if (specCount > 0) parts.push(`${specCount} spec`); + if (manualCount > 0) parts.push(`${manualCount} manual`); + text += ` (${parts.join(', ')})`; + } + this.statusBarEl.setText(text); + } + + updateRibbonIcon() { + if (!this.ribbonIconEl) return; + if (this.settings.suspended) { + this.ribbonIconEl.addClass('abacus-ribbon-suspended'); + this.ribbonIconEl.setAttribute('aria-label', 'Abacus Custom Sorter (suspended)'); + } else { + this.ribbonIconEl.removeClass('abacus-ribbon-suspended'); + this.ribbonIconEl.setAttribute('aria-label', 'Abacus Custom Sorter (active)'); + } + } +} + +module.exports = AbacusSorterPlugin; diff --git a/.obsidian/plugins/abacus-sorter/manifest.json b/.obsidian/plugins/abacus-sorter/manifest.json new file mode 100644 index 0000000..1d35c98 --- /dev/null +++ b/.obsidian/plugins/abacus-sorter/manifest.json @@ -0,0 +1,9 @@ +{ + "id": "abacus-sorter", + "name": "Abacus Custom Sorter", + "version": "1.0.0", + "minAppVersion": "1.7.2", + "description": "Config-driven custom sorting of files and folders in File Explorer, with manual drag-style reordering and sort specification support.", + "author": "Conway", + "isDesktopOnly": false +} diff --git a/.obsidian/plugins/abacus-sorter/styles.css b/.obsidian/plugins/abacus-sorter/styles.css new file mode 100644 index 0000000..599055a --- /dev/null +++ b/.obsidian/plugins/abacus-sorter/styles.css @@ -0,0 +1,139 @@ +/* Abacus Custom Sorter */ + +/* Ribbon icon when suspended */ +.abacus-ribbon-suspended { + opacity: 0.4; +} + +/* Manual Order Modal */ +.abacus-manual-order-modal { + max-width: 500px; +} + +.abacus-modal-subtitle { + color: var(--text-muted); + margin-bottom: 4px; +} + +.abacus-modal-hint { + color: var(--text-faint); + font-size: var(--font-smallest); + margin-bottom: 12px; +} + +.abacus-order-list { + max-height: 400px; + overflow-y: auto; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + margin-bottom: 12px; +} + +.abacus-order-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-bottom: 1px solid var(--background-modifier-border); + cursor: grab; + transition: background-color 0.1s; +} + +.abacus-order-row:last-child { + border-bottom: none; +} + +.abacus-order-row:hover { + background-color: var(--background-modifier-hover); +} + +.abacus-order-row.abacus-dragging { + opacity: 0.4; +} + +.abacus-order-row.abacus-drag-over { + background-color: var(--interactive-accent); + color: var(--text-on-accent); +} + +.abacus-drag-handle { + color: var(--text-faint); + cursor: grab; + user-select: none; + font-size: 14px; + flex-shrink: 0; +} + +.abacus-order-index { + color: var(--text-muted); + font-size: var(--font-smallest); + min-width: 20px; + text-align: right; + flex-shrink: 0; +} + +.abacus-order-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.abacus-order-buttons { + display: flex; + gap: 2px; + flex-shrink: 0; +} + +.abacus-order-btn { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 2px 4px; + font-size: 10px; + border-radius: 3px; +} + +.abacus-order-btn:hover { + background-color: var(--background-modifier-hover); + color: var(--text-normal); +} + +.abacus-modal-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +/* Quick Sort Modal */ +.abacus-quick-sort-modal { + max-width: 350px; +} + +.abacus-quick-sort-list { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 12px; +} + +.abacus-quick-sort-option { + text-align: left; + padding: 8px 12px; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + background: var(--background-secondary); + cursor: pointer; + transition: background-color 0.1s; +} + +.abacus-quick-sort-option:hover { + background-color: var(--interactive-accent); + color: var(--text-on-accent); +} + +.abacus-quick-sort-cancel { + margin-top: 4px; + align-self: flex-end; +} diff --git a/.obsidian/themes/Spectroscope-Gruv/manifest.json b/.obsidian/themes/Spectroscope-Gruv/manifest.json new file mode 100644 index 0000000..4655e59 --- /dev/null +++ b/.obsidian/themes/Spectroscope-Gruv/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "Spectroscope-Gruv", + "version": "1.0.0", + "minAppVersion": "0.16.0", + "author": "Conway", + "authorUrl": "", + "id": "Spectroscope-Gruv", + "isDesktopOnly": false +} diff --git a/.obsidian/themes/Spectroscope-Gruv/theme.css b/.obsidian/themes/Spectroscope-Gruv/theme.css new file mode 100644 index 0000000..5fe67ec --- /dev/null +++ b/.obsidian/themes/Spectroscope-Gruv/theme.css @@ -0,0 +1,473 @@ +/* +Spectroscope-Gruv is based on Gruvbox Darker, with darker backgrounds from Minimal-Dark-Coder. Zero light mode support. +*/ + +:root +{ + /* DARKENED BACKGROUND SCALE - shifted down to match Minimal-Dark-Coder depth */ + --dark0-hard_x: 24,26,27; /* #181a1b - matches Minimal-Dark-Coder */ + --dark0-hard: rgb(var(--dark0-hard_x)); + --dark0_x: 32,32,32; /* #202020 - was #282828 */ + --dark0: rgb(var(--dark0_x)); + --dark0-soft_x: 40,38,37; /* #282625 - was #32302f */ + --dark0-soft: rgb(var(--dark0-soft_x)); + --dark1_x: 50,46,44; /* #322e2c - was #3c3836 */ + --dark1: rgb(var(--dark1_x)); + --dark2_x: 70,63,59; /* #463f3b - was #504945 */ + --dark2: rgb(var(--dark2_x)); + --dark3_x: 92,82,74; /* #5c524a - was #665c54 */ + --dark3: rgb(var(--dark3_x)); + --dark4_x: 114,101,90; /* #72655a - was #7c6f64 */ + --dark4: rgb(var(--dark4_x)); + --gray_x: 146,131,116; /* #928374 - unchanged */ + --gray: rgb(var(--gray_x)); + + /* LIGHT SCALE - unchanged, used for text */ + --light0-hard_x: 249,245,215; /* #f9f5d7 */ + --light0-hard: rgb(var(--light0-hard_x)); + --light0_x: 251,241,199; /* #fbf1c7 */ + --light0: rgb(var(--light0_x)); + --light0-soft_x: 242,229,188; /* #f2e5bc */ + --light0-soft: rgb(var(--light0-soft_x)); + --light1_x: 235,219,178; /* #ebdbb2 */ + --light1: rgb(var(--light1_x)); + --light2_x: 213,196,161; /* #d5c4a1 */ + --light2: rgb(var(--light2_x)); + --light3_x: 189,174,147; /* #bdae93 */ + --light3: rgb(var(--light3_x)); + --light4_x: 168,153,132; /* #a89984 */ + --light4: rgb(var(--light4_x)); + + /* BRIGHT COLORS - unchanged */ + --bright-red_x: 251,73,52; /* #fb4934 */ + --bright-red: rgb(var(--bright-red_x)); + --bright-green_x: 184,187,38; /* #b8bb26 */ + --bright-green: rgb(var(--bright-green_x)); + --bright-yellow_x: 250,189,47; /* #fabd2f */ + --bright-yellow: rgb(var(--bright-yellow_x)); + --bright-blue_x: 131,165,152; /* #83a598 */ + --bright-blue: rgb(var(--bright-blue_x)); + --bright-purple_x: 211,134,155; /* #d3869b */ + --bright-purple: rgb(var(--bright-purple_x)); + --bright-aqua_x: 142,192,124; /* #8ec07c */ + --bright-aqua: rgb(var(--bright-aqua_x)); + --bright-orange_x: 254,128,25; /* #fe8019 */ + --bright-orange: rgb(var(--bright-orange_x)); + + /* NEUTRAL COLORS - unchanged */ + --neutral-red_x: 204,36,29; /* #cc241d */ + --neutral-red: rgb(var(--neutral-red_x)); + --neutral-green_x: 152,151,26; /* #98971a */ + --neutral-green: rgb(var(--neutral-green_x)); + --neutral-yellow_x: 215,153,33; /* #d79921 */ + --neutral-yellow: rgb(var(--neutral-yellow_x)); + --neutral-blue_x: 69,133,136; /* #458588 */ + --neutral-blue: rgb(var(--neutral-blue_x)); + --neutral-purple_x: 177,98,134; /* #b16286 */ + --neutral-purple: rgb(var(--neutral-purple_x)); + --neutral-aqua_x: 104,157,106; /* #689d6a */ + --neutral-aqua: rgb(var(--neutral-aqua_x)); + --neutral-orange_x: 214,93,14; /* #d65d0e */ + --neutral-orange: rgb(var(--neutral-orange_x)); + + /* FADED COLORS - unchanged */ + --faded-red_x: 157,0,6; /* #9d0006 */ + --faded-red: rgb(var(--faded-red_x)); + --faded-green_x: 121,116,14; /* #79740e */ + --faded-green: rgb(var(--faded-green_x)); + --faded-yellow_x: 181,118,20; /* #b57614 */ + --faded-yellow: rgb(var(--faded-yellow_x)); + --faded-blue_x: 7,102,120; /* #076678 */ + --faded-blue: rgb(var(--faded-blue_x)); + --faded-purple_x: 143,63,113; /* #8f3f71 */ + --faded-purple: rgb(var(--faded-purple_x)); + --faded-aqua_x: 66,123,88; /* #427b58 */ + --faded-aqua: rgb(var(--faded-aqua_x)); + --faded-orange_x: 175,58,3; /* #af3a03 */ + --faded-orange: rgb(var(--faded-orange_x)); +} + +body +{ + --accent-h: 12; /* --faded-red #9d0006 */ + --accent-s: 107%; + --accent-l: 32%; + + --link-decoration: none; + --link-decoration-hover: none; + --link-external-decoration: none; + --link-external-decoration-hover: none; + + --tag-decoration: none; + --tag-decoration-hover: underline; + --tag-padding-x: .5em; + --tag-padding-y: .2em; + --tag-radius: .5em; + + --tab-font-weight: 600; + --bold-weight: 600; + + --checkbox-radius: 0; + + --embed-border-left: 6px double var(--interactive-accent); +} + +.theme-dark +{ + --color-red-rgb: var(--neutral-red_x); + --color-red: var(--neutral-red); + --color-purple-rgb: var(--neutral-purple_x); + --color-purple: var(--neutral-purple); + --color-green-rgb: var(--neutral-green_x); + --color-green: var(--neutral-green); + --color-cyan-rgb: var(--neutral-blue_x); + --color-cyan: var(--neutral-blue); + --color-blue-rgb: var(--faded-blue_x); + --color-blue: var(--faded-blue); + --color-yellow-rgb: var(--neutral-yellow_x); + --color-yellow: var(--neutral-yellow); + --color-orange-rgb: var(--neutral-orange_x); + --color-orange: var(--neutral-orange); + --color-pink-rgb: var(--bright-purple_x); + --color-pink: var(--bright-purple); + + --background-primary: var(--dark0); + --background-primary-alt: var(--dark0); + --background-secondary: var(--dark0-hard); + --background-secondary-alt: var(--dark1); + --background-modifier-border: var(--dark1); + + /* Adjusted for darker backgrounds - subtle highlight */ + --cursor-line-background: rgba(var(--dark1_x), 0.4); + + --text-normal: var(--light0); + --text-faint: var(--light1); + --text-muted: var(--light2); + + --link-url: var(--neutral-green); + + /* Header colors - rainbow progression */ + --h1-color: var(--neutral-red); + --h2-color: var(--neutral-yellow); + --h3-color: var(--neutral-green); + --h4-color: var(--neutral-aqua); + --h5-color: var(--neutral-blue); + --h6-color: var(--neutral-purple); + + --text-highlight-bg: var(--neutral-yellow); + --text-highlight-fg: var(--dark0-hard); + + --text-accent: var(--neutral-orange); + --text-accent-hover: var(--bright-aqua); + + --tag-color: var(--bright-aqua); + --tag-background: var(--dark2); + --tag-background-hover: var(--dark1); + + --titlebar-text-color-focused: var(--bright-red); + + --inline-title-color: var(--bright-yellow); + + --bold-color: var(--neutral-yellow); + --italic-color: var(--light4); + + --checkbox-color: var(--light4); + --checkbox-color-hover: var(--light4); + --checkbox-border-color: var(--light4); + --checkbox-border-color-hover: var(--light4); + --checklist-done-color: rgba(var(--light2_x), 0.5); + + --table-header-background: rgba(var(--dark0_x), 0.2); + --table-header-background-hover: var(--dark2); + --table-row-even-background: rgba(var(--dark2_x), 0.2); + --table-row-odd-background: rgba(var(--dark2_x), 0.4); + --table-row-background-hover: var(--dark2); + + --text-selection: rgba(var(--neutral-red_x), 0.6); + --flashing-background: rgba(var(--neutral-red_x), 0.3); + + --code-normal: var(--bright-blue); + --code-background: var(--dark1); + + --mermaid-note: var(--neutral-blue); + --mermaid-actor: var(--dark2); + --mermaid-loopline: var(--neutral-blue); + --mermaid-exclude: var(--dark4); + --mermaid-seqnum: var(--dark0); + + --icon-color-hover: var(--bright-red); + --icon-color-focused: var(--bright-blue); + + --nav-item-color-hover: var(--bright-red); + --nav-item-color-active: var(--bright-aqua); + --nav-file-tag: rgba(var(--neutral-yellow_x), 0.9); + + --graph-line: var(--dark2); + --graph-node: var(--light3); + --graph-node-tag: var(--neutral-red); + --graph-node-attachment: var(--neutral-green); + + --calendar-hover: var(--bright-red); + --calendar-background-hover: var(--dark1); + --calendar-week: var(--neutral-orange); + --calendar-today: var(--neutral-orange); + + --dataview-key: var(--text-faint); + --dataview-key-background: rgba(var(--faded-red_x), 0.5); + --dataview-value: var(--text-faint); + --dataview-value-background: rgba(var(--neutral-green_x), 0.3); + + --tab-text-color-focused-active: var(--neutral-yellow); + --tab-text-color-focused-active-current: var(--bright-red); +} + +/* TABLE STYLING */ +table +{ + border: 1px solid var(--background-secondary) !important; + border-collapse: collapse; +} + +thead +{ + border-bottom: 2px solid var(--background-modifier-border) !important; +} + +th +{ + font-weight: 600 !important; + border: 1px solid var(--background-secondary) !important; +} + +td +{ + border-left: 1px solid var(--background-secondary) !important; + border-right: 1px solid var(--background-secondary) !important; + border-bottom: 1px solid var(--background-secondary) !important; +} + +.markdown-rendered tbody tr:nth-child(even) +{ + background-color: var(--table-row-even-background) !important; +} + +.markdown-rendered tbody tr:nth-child(odd) +{ + background-color: var(--table-row-odd-background) !important; +} + +.markdown-rendered tbody tr:nth-child(even):hover, +.markdown-rendered tbody tr:nth-child(odd):hover +{ + background-color: var(--table-row-background-hover) !important; +} + +/* HIGHLIGHT/MARK STYLING */ +.markdown-rendered mark +{ + background-color: var(--text-highlight-bg); + color: var(--text-highlight-fg); +} + +.markdown-rendered mark a +{ + color: var(--red) !important; + font-weight: 600; +} + +.search-result-file-matched-text +{ + color: var(--text-highlight-fg) !important; +} + +/* TAG HOVER */ +.cm-hashtag-begin:hover, .cm-hashtag-end:hover +{ + color: var(--text-accent); + text-decoration: underline; +} + +/* CHECKBOX STYLING */ +input[type=checkbox] +{ + border: 1px solid var(--checkbox-color); +} + +input[type=checkbox]:checked +{ + background-color: var(--checkbox-color); + box-shadow: inset 0 0 0 2px var(--background-primary); +} + +input[type=checkbox]:checked:after +{ + display: none; +} + +/* CODE BLOCKS */ +code[class*="language-"], +pre[class*="language-"] +{ + line-height: var(--line-height-tight) !important; +} + +/* URL/LINK STYLING */ +.cm-url +{ + color: var(--link-url) !important; +} + +.cm-url:hover +{ + color: var(--text-accent-color) !important; +} + +/* EDITOR-PREVIEW CONSISTENCY */ +.cm-highlight +{ + color: var(--text-highlight-fg) !important; +} + +.cm-inline-code +{ + border-radius: var(--radius-s); + font-size: var(--code-size); + padding: 0.1em 0.25em; +} + +.cm-line .cm-strong +{ + color: var(--bold-color) !important; +} + +/* MERMAID DIAGRAMS */ +.mermaid .note +{ + fill: var(--mermaid-note) !important; +} + +.mermaid .actor +{ + fill: var(--mermaid-actor) !important; +} + +.mermaid .loopLine +{ + stroke: var(--mermaid-loopline) !important; +} + +.mermaid .loopText>tspan, +.mermaid .entityLabel +{ + fill: var(--neutral-red) !important; +} + +.mermaid .exclude-range +{ + fill: var(--mermaid-exclude) !important; +} + +.mermaid .sequenceNumber +{ + fill: var(--mermaid-seqnum) !important; +} + +/* CALENDAR PLUGIN */ +.calendar .week-num +{ + color: var(--calendar-week) !important; +} + +.calendar .today +{ + color: var(--calendar-today) !important; +} + +.calendar .week-num:hover, +.calendar .day:hover +{ + color: var(--calendar-hover) !important; + background-color: var(--calendar-background-hover) !important; +} + +/* EMBEDS */ +.markdown-embed-title +{ + color: var(--yellow); + font-weight: 600 !important; +} + +/* ACTIVE LINE */ +.cm-active +{ + background-color: var(--cursor-line-background) !important; +} + +/* FILE EXPLORER */ +.nav-file-tag +{ + color: var(--nav-file-tag) !important; +} + +.is-flashing +{ + background-color: var(--flashing-background) !important; +} + +/* DATAVIEW PLUGIN */ +.dataview.inline-field-key +{ + border-top-left-radius: var(--radius-s); + border-bottom-left-radius: var(--radius-s); + padding-left: 4px; + font-family: var(--font-monospace); + font-size: var(--font-smaller); + color: var(--dataview-key) !important; + background-color: var(--dataview-key-background) !important; +} + +.dataview.inline-field-value +{ + border-top-right-radius: var(--radius-s); + border-bottom-right-radius: var(--radius-s); + padding-right: 4px; + font-family: var(--font-monospace); + font-size: var(--font-smaller); + color: var(--dataview-value) !important; + background-color: var(--dataview-value-background) !important; +} + +/* SUGGESTION POPUP */ +.suggestion-highlight +{ + color: var(--bright-red); +} + +/* CALLOUTS */ +body { + --callout-border-width: 1px; + --callout-border-opacity: 0.4; + --callout-default: var(--neutral-blue_x); + --callout-note: var(--neutral-blue_x); + --callout-summary: var(--neutral-aqua_x); + --callout-info: var(--neutral-blue_x); + --callout-todo: var(--neutral-blue_x); + --callout-important: var(--neutral-aqua_x); + --callout-tip: var(--neutral-aqua_x); + --callout-success: var(--neutral-green_x); + --callout-question: var(--neutral-yellow_x); + --callout-warning: var(--neutral-orange_x); + --callout-fail: var(--neutral-red_x); + --callout-error: var(--neutral-red_x); + --callout-bug: var(--neutral-red_x); + --callout-example: var(--neutral-purple_x); + --callout-quote: var(--gray_x); +} + +/* + +.callout { + background-color: rgba(var(--callout-color), 0.2); +} + +/* */ diff --git a/.obsidian/themes/Spectroscope-Noir/manifest.json b/.obsidian/themes/Spectroscope-Noir/manifest.json new file mode 100644 index 0000000..45ffdd2 --- /dev/null +++ b/.obsidian/themes/Spectroscope-Noir/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "Spectroscope-Noir", + "version": "1.0.0", + "minAppVersion": "0.16.0", + "author": "Conway", + "authorUrl": "", + "id": "Spectroscope-Noir", + "isDesktopOnly": false +} diff --git a/.obsidian/themes/Spectroscope-Noir/theme.css b/.obsidian/themes/Spectroscope-Noir/theme.css new file mode 100644 index 0000000..bdc423e --- /dev/null +++ b/.obsidian/themes/Spectroscope-Noir/theme.css @@ -0,0 +1,474 @@ +/* +Spectroscope-Noir is based on Spectroscope-Gruv, with pure black backgrounds inspired by the Blackbird theme. +Still zero light mode support. +*/ + +:root +{ + /* PURE BLACK BACKGROUND SCALE - neutral, no warm tint */ + --dark0-hard_x: 0,0,0; /* #000000 - pure black */ + --dark0-hard: rgb(var(--dark0-hard_x)); + --dark0_x: 10,10,10; /* #0a0a0a - near black */ + --dark0: rgb(var(--dark0_x)); + --dark0-soft_x: 18,18,18; /* #121212 */ + --dark0-soft: rgb(var(--dark0-soft_x)); + --dark1_x: 26,26,26; /* #1a1a1a */ + --dark1: rgb(var(--dark1_x)); + --dark2_x: 37,37,37; /* #252525 */ + --dark2: rgb(var(--dark2_x)); + --dark3_x: 51,51,51; /* #333333 */ + --dark3: rgb(var(--dark3_x)); + --dark4_x: 68,68,68; /* #444444 */ + --dark4: rgb(var(--dark4_x)); + --gray_x: 146,131,116; /* #928374 - unchanged */ + --gray: rgb(var(--gray_x)); + + /* LIGHT SCALE - unchanged, used for text */ + --light0-hard_x: 249,245,215; /* #f9f5d7 */ + --light0-hard: rgb(var(--light0-hard_x)); + --light0_x: 251,241,199; /* #fbf1c7 */ + --light0: rgb(var(--light0_x)); + --light0-soft_x: 242,229,188; /* #f2e5bc */ + --light0-soft: rgb(var(--light0-soft_x)); + --light1_x: 235,219,178; /* #ebdbb2 */ + --light1: rgb(var(--light1_x)); + --light2_x: 213,196,161; /* #d5c4a1 */ + --light2: rgb(var(--light2_x)); + --light3_x: 189,174,147; /* #bdae93 */ + --light3: rgb(var(--light3_x)); + --light4_x: 168,153,132; /* #a89984 */ + --light4: rgb(var(--light4_x)); + + /* BRIGHT COLORS - unchanged */ + --bright-red_x: 251,73,52; /* #fb4934 */ + --bright-red: rgb(var(--bright-red_x)); + --bright-green_x: 184,187,38; /* #b8bb26 */ + --bright-green: rgb(var(--bright-green_x)); + --bright-yellow_x: 250,189,47; /* #fabd2f */ + --bright-yellow: rgb(var(--bright-yellow_x)); + --bright-blue_x: 131,165,152; /* #83a598 */ + --bright-blue: rgb(var(--bright-blue_x)); + --bright-purple_x: 211,134,155; /* #d3869b */ + --bright-purple: rgb(var(--bright-purple_x)); + --bright-aqua_x: 142,192,124; /* #8ec07c */ + --bright-aqua: rgb(var(--bright-aqua_x)); + --bright-orange_x: 254,128,25; /* #fe8019 */ + --bright-orange: rgb(var(--bright-orange_x)); + + /* NEUTRAL COLORS - unchanged */ + --neutral-red_x: 204,36,29; /* #cc241d */ + --neutral-red: rgb(var(--neutral-red_x)); + --neutral-green_x: 152,151,26; /* #98971a */ + --neutral-green: rgb(var(--neutral-green_x)); + --neutral-yellow_x: 215,153,33; /* #d79921 */ + --neutral-yellow: rgb(var(--neutral-yellow_x)); + --neutral-blue_x: 69,133,136; /* #458588 */ + --neutral-blue: rgb(var(--neutral-blue_x)); + --neutral-purple_x: 177,98,134; /* #b16286 */ + --neutral-purple: rgb(var(--neutral-purple_x)); + --neutral-aqua_x: 104,157,106; /* #689d6a */ + --neutral-aqua: rgb(var(--neutral-aqua_x)); + --neutral-orange_x: 214,93,14; /* #d65d0e */ + --neutral-orange: rgb(var(--neutral-orange_x)); + + /* FADED COLORS - unchanged */ + --faded-red_x: 157,0,6; /* #9d0006 */ + --faded-red: rgb(var(--faded-red_x)); + --faded-green_x: 121,116,14; /* #79740e */ + --faded-green: rgb(var(--faded-green_x)); + --faded-yellow_x: 181,118,20; /* #b57614 */ + --faded-yellow: rgb(var(--faded-yellow_x)); + --faded-blue_x: 7,102,120; /* #076678 */ + --faded-blue: rgb(var(--faded-blue_x)); + --faded-purple_x: 143,63,113; /* #8f3f71 */ + --faded-purple: rgb(var(--faded-purple_x)); + --faded-aqua_x: 66,123,88; /* #427b58 */ + --faded-aqua: rgb(var(--faded-aqua_x)); + --faded-orange_x: 175,58,3; /* #af3a03 */ + --faded-orange: rgb(var(--faded-orange_x)); +} + +body +{ + --accent-h: 12; /* --faded-red #9d0006 */ + --accent-s: 107%; + --accent-l: 32%; + + --link-decoration: none; + --link-decoration-hover: none; + --link-external-decoration: none; + --link-external-decoration-hover: none; + + --tag-decoration: none; + --tag-decoration-hover: underline; + --tag-padding-x: .5em; + --tag-padding-y: .2em; + --tag-radius: .5em; + + --tab-font-weight: 600; + --bold-weight: 600; + + --checkbox-radius: 0; + + --embed-border-left: 6px double var(--interactive-accent); +} + +.theme-dark +{ + --color-red-rgb: var(--neutral-red_x); + --color-red: var(--neutral-red); + --color-purple-rgb: var(--neutral-purple_x); + --color-purple: var(--neutral-purple); + --color-green-rgb: var(--neutral-green_x); + --color-green: var(--neutral-green); + --color-cyan-rgb: var(--neutral-blue_x); + --color-cyan: var(--neutral-blue); + --color-blue-rgb: var(--faded-blue_x); + --color-blue: var(--faded-blue); + --color-yellow-rgb: var(--neutral-yellow_x); + --color-yellow: var(--neutral-yellow); + --color-orange-rgb: var(--neutral-orange_x); + --color-orange: var(--neutral-orange); + --color-pink-rgb: var(--bright-purple_x); + --color-pink: var(--bright-purple); + + --background-primary: var(--dark0); + --background-primary-alt: var(--dark0); + --background-secondary: var(--dark0-hard); + --background-secondary-alt: var(--dark1); + --background-modifier-border: var(--dark1); + + /* Adjusted for black backgrounds - subtle highlight */ + --cursor-line-background: rgba(var(--dark1_x), 0.4); + + --text-normal: var(--light0); + --text-faint: var(--light1); + --text-muted: var(--light2); + + --link-url: var(--neutral-green); + + /* Header colors - rainbow progression */ + --h1-color: var(--neutral-red); + --h2-color: var(--neutral-yellow); + --h3-color: var(--neutral-green); + --h4-color: var(--neutral-aqua); + --h5-color: var(--neutral-blue); + --h6-color: var(--neutral-purple); + + --text-highlight-bg: var(--neutral-yellow); + --text-highlight-fg: var(--dark0-hard); + + --text-accent: var(--neutral-orange); + --text-accent-hover: var(--bright-aqua); + + --tag-color: var(--bright-aqua); + --tag-background: var(--dark2); + --tag-background-hover: var(--dark1); + + --titlebar-text-color-focused: var(--bright-red); + + --inline-title-color: var(--bright-yellow); + + --bold-color: var(--neutral-yellow); + --italic-color: var(--light4); + + --checkbox-color: var(--light4); + --checkbox-color-hover: var(--light4); + --checkbox-border-color: var(--light4); + --checkbox-border-color-hover: var(--light4); + --checklist-done-color: rgba(var(--light2_x), 0.5); + + --table-header-background: rgba(var(--dark0_x), 0.2); + --table-header-background-hover: var(--dark2); + --table-row-even-background: rgba(var(--dark2_x), 0.2); + --table-row-odd-background: rgba(var(--dark2_x), 0.4); + --table-row-background-hover: var(--dark2); + + --text-selection: rgba(var(--neutral-red_x), 0.6); + --flashing-background: rgba(var(--neutral-red_x), 0.3); + + --code-normal: var(--bright-blue); + --code-background: var(--dark1); + + --mermaid-note: var(--neutral-blue); + --mermaid-actor: var(--dark2); + --mermaid-loopline: var(--neutral-blue); + --mermaid-exclude: var(--dark4); + --mermaid-seqnum: var(--dark0); + + --icon-color-hover: var(--bright-red); + --icon-color-focused: var(--bright-blue); + + --nav-item-color-hover: var(--bright-red); + --nav-item-color-active: var(--bright-aqua); + --nav-file-tag: rgba(var(--neutral-yellow_x), 0.9); + + --graph-line: var(--dark2); + --graph-node: var(--light3); + --graph-node-tag: var(--neutral-red); + --graph-node-attachment: var(--neutral-green); + + --calendar-hover: var(--bright-red); + --calendar-background-hover: var(--dark1); + --calendar-week: var(--neutral-orange); + --calendar-today: var(--neutral-orange); + + --dataview-key: var(--text-faint); + --dataview-key-background: rgba(var(--faded-red_x), 0.5); + --dataview-value: var(--text-faint); + --dataview-value-background: rgba(var(--neutral-green_x), 0.3); + + --tab-text-color-focused-active: var(--neutral-yellow); + --tab-text-color-focused-active-current: var(--bright-red); +} + +/* TABLE STYLING */ +table +{ + border: 1px solid var(--background-secondary) !important; + border-collapse: collapse; +} + +thead +{ + border-bottom: 2px solid var(--background-modifier-border) !important; +} + +th +{ + font-weight: 600 !important; + border: 1px solid var(--background-secondary) !important; +} + +td +{ + border-left: 1px solid var(--background-secondary) !important; + border-right: 1px solid var(--background-secondary) !important; + border-bottom: 1px solid var(--background-secondary) !important; +} + +.markdown-rendered tbody tr:nth-child(even) +{ + background-color: var(--table-row-even-background) !important; +} + +.markdown-rendered tbody tr:nth-child(odd) +{ + background-color: var(--table-row-odd-background) !important; +} + +.markdown-rendered tbody tr:nth-child(even):hover, +.markdown-rendered tbody tr:nth-child(odd):hover +{ + background-color: var(--table-row-background-hover) !important; +} + +/* HIGHLIGHT/MARK STYLING */ +.markdown-rendered mark +{ + background-color: var(--text-highlight-bg); + color: var(--text-highlight-fg); +} + +.markdown-rendered mark a +{ + color: var(--red) !important; + font-weight: 600; +} + +.search-result-file-matched-text +{ + color: var(--text-highlight-fg) !important; +} + +/* TAG HOVER */ +.cm-hashtag-begin:hover, .cm-hashtag-end:hover +{ + color: var(--text-accent); + text-decoration: underline; +} + +/* CHECKBOX STYLING */ +input[type=checkbox] +{ + border: 1px solid var(--checkbox-color); +} + +input[type=checkbox]:checked +{ + background-color: var(--checkbox-color); + box-shadow: inset 0 0 0 2px var(--background-primary); +} + +input[type=checkbox]:checked:after +{ + display: none; +} + +/* CODE BLOCKS */ +code[class*="language-"], +pre[class*="language-"] +{ + line-height: var(--line-height-tight) !important; +} + +/* URL/LINK STYLING */ +.cm-url +{ + color: var(--link-url) !important; +} + +.cm-url:hover +{ + color: var(--text-accent-color) !important; +} + +/* EDITOR-PREVIEW CONSISTENCY */ +.cm-highlight +{ + color: var(--text-highlight-fg) !important; +} + +.cm-inline-code +{ + border-radius: var(--radius-s); + font-size: var(--code-size); + padding: 0.1em 0.25em; +} + +.cm-line .cm-strong +{ + color: var(--bold-color) !important; +} + +/* MERMAID DIAGRAMS */ +.mermaid .note +{ + fill: var(--mermaid-note) !important; +} + +.mermaid .actor +{ + fill: var(--mermaid-actor) !important; +} + +.mermaid .loopLine +{ + stroke: var(--mermaid-loopline) !important; +} + +.mermaid .loopText>tspan, +.mermaid .entityLabel +{ + fill: var(--neutral-red) !important; +} + +.mermaid .exclude-range +{ + fill: var(--mermaid-exclude) !important; +} + +.mermaid .sequenceNumber +{ + fill: var(--mermaid-seqnum) !important; +} + +/* CALENDAR PLUGIN */ +.calendar .week-num +{ + color: var(--calendar-week) !important; +} + +.calendar .today +{ + color: var(--calendar-today) !important; +} + +.calendar .week-num:hover, +.calendar .day:hover +{ + color: var(--calendar-hover) !important; + background-color: var(--calendar-background-hover) !important; +} + +/* EMBEDS */ +.markdown-embed-title +{ + color: var(--yellow); + font-weight: 600 !important; +} + +/* ACTIVE LINE */ +.cm-active +{ + background-color: var(--cursor-line-background) !important; +} + +/* FILE EXPLORER */ +.nav-file-tag +{ + color: var(--nav-file-tag) !important; +} + +.is-flashing +{ + background-color: var(--flashing-background) !important; +} + +/* DATAVIEW PLUGIN */ +.dataview.inline-field-key +{ + border-top-left-radius: var(--radius-s); + border-bottom-left-radius: var(--radius-s); + padding-left: 4px; + font-family: var(--font-monospace); + font-size: var(--font-smaller); + color: var(--dataview-key) !important; + background-color: var(--dataview-key-background) !important; +} + +.dataview.inline-field-value +{ + border-top-right-radius: var(--radius-s); + border-bottom-right-radius: var(--radius-s); + padding-right: 4px; + font-family: var(--font-monospace); + font-size: var(--font-smaller); + color: var(--dataview-value) !important; + background-color: var(--dataview-value-background) !important; +} + +/* SUGGESTION POPUP */ +.suggestion-highlight +{ + color: var(--bright-red); +} + +/* CALLOUTS */ +body { + --callout-border-width: 1px; + --callout-border-opacity: 0.4; + --callout-default: var(--neutral-blue_x); + --callout-note: var(--neutral-blue_x); + --callout-summary: var(--neutral-aqua_x); + --callout-info: var(--neutral-blue_x); + --callout-todo: var(--neutral-blue_x); + --callout-important: var(--neutral-aqua_x); + --callout-tip: var(--neutral-aqua_x); + --callout-success: var(--neutral-green_x); + --callout-question: var(--neutral-yellow_x); + --callout-warning: var(--neutral-orange_x); + --callout-fail: var(--neutral-red_x); + --callout-error: var(--neutral-red_x); + --callout-bug: var(--neutral-red_x); + --callout-example: var(--neutral-purple_x); + --callout-quote: var(--gray_x); +} + +/* + +.callout { + background-color: rgba(var(--callout-color), 0.2); +} + +/* */ -- cgit v1.3