diff --git a/README.md b/README.md index a17fdc7..dbe1d9b 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,9 @@ Examples of how people use it: 1. Go to **Settings → Color Tab**. 2. Each of the 5 color slots has a **name field** and a **color picker**. -3. Changes take effect immediately — open tabs update live. -4. Click **Reset** to restore the default pastel palette. +3. Toggle **Prevent Tab Duplication** to keep only one tab per file. +4. Changes take effect immediately — open tabs update live. +5. Click **Reset** to restore the default pastel palette. ## Default colors diff --git a/main.ts b/main.ts index 1c7349a..9996eb9 100644 --- a/main.ts +++ b/main.ts @@ -17,6 +17,7 @@ interface ColorTabSettings { /** Maps file path → hex color */ fileColors: Record; autoPinColoredTabs: boolean; + preventTabDuplication: boolean; } const DEFAULT_COLORS: ColorEntry[] = [ @@ -31,6 +32,7 @@ const DEFAULT_SETTINGS: ColorTabSettings = { colors: DEFAULT_COLORS, fileColors: {}, autoPinColoredTabs: true, + preventTabDuplication: true, }; export default class ColorTabPlugin extends Plugin { @@ -53,20 +55,29 @@ export default class ColorTabPlugin extends Plugin { // Re-apply stored colors whenever the layout changes this.registerEvent( - this.app.workspace.on("layout-change", () => - this.applyAllColors() - ) + this.app.workspace.on("layout-change", () => { + this.applyAllColors(); + if (this.settings.preventTabDuplication) { + this.handleDuplicateTabs(); + } + }) ); // Clear/apply color when a new file is loaded into any leaf this.registerEvent( - this.app.workspace.on("active-leaf-change", () => - this.applyAllColors() - ) + this.app.workspace.on("active-leaf-change", () => { + this.applyAllColors(); + if (this.settings.preventTabDuplication) { + this.handleDuplicateTabs(); + } + }) ); this.app.workspace.onLayoutReady(() => { this.applyAllColors(); + if (this.settings.preventTabDuplication) { + this.handleDuplicateTabs(); + } this.registerColorCommands(); }); } @@ -222,6 +233,45 @@ export default class ColorTabPlugin extends Plugin { }); } + // ── Tab duplication prevention ──────────────────────────────────────────── + + /** + * Detects and handles duplicate tabs (same file open in multiple tabs). + * Closes duplicate tabs and focuses on the first instance. + * Works with both regular and colored (pinned) tabs. + */ + private handleDuplicateTabs() { + const filePathMap = new Map(); + + // Build a map of file paths to their corresponding leaves + this.app.workspace.iterateAllLeaves((leaf) => { + const path = this.getFilePath(leaf); + if (path) { + if (!filePathMap.has(path)) { + filePathMap.set(path, []); + } + filePathMap.get(path)!.push(leaf); + } + }); + + // For each file with duplicates, keep the first and close the rest + filePathMap.forEach((leaves) => { + if (leaves.length > 1) { + // Keep the first leaf and focus on it + const firstLeaf = leaves[0]; + this.app.workspace.setActiveLeaf(firstLeaf); + + // Close the duplicate leaves + for (let i = 1; i < leaves.length; i++) { + const duplicateLeaf = leaves[i]; + // Unpin the duplicate tab before closing it (important for colored tabs) + duplicateLeaf.setPinned(false); + duplicateLeaf.detach(); + } + } + }); + } + // ── Helpers ─────────────────────────────────────────────────────────────── private getFilePath(leaf: WorkspaceLeaf): string | null { @@ -252,6 +302,7 @@ export default class ColorTabPlugin extends Plugin { colors: saved?.colors ?? DEFAULT_COLORS.map((c) => ({ ...c })), fileColors: saved?.fileColors ?? {}, autoPinColoredTabs: saved?.autoPinColoredTabs ?? DEFAULT_SETTINGS.autoPinColoredTabs, + preventTabDuplication: saved?.preventTabDuplication ?? DEFAULT_SETTINGS.preventTabDuplication, }; } @@ -353,6 +404,11 @@ class ColorTabSettingTab extends PluginSettingTab { desc: "When enabled, applying a tab color pins the tab and removing color unpins it.", control: { type: "toggle" as const, key: "autoPinColoredTabs" }, }, + { + name: "Prevent Tab Duplication", + desc: "When enabled, opening an already-open file will focus on the existing tab instead of creating a duplicate.", + control: { type: "toggle" as const, key: "preventTabDuplication" }, + }, { name: "Reset to defaults", desc: "Restore the original pastel color palette.", @@ -366,6 +422,8 @@ class ColorTabSettingTab extends PluginSettingTab { ); this.plugin.settings.autoPinColoredTabs = DEFAULT_SETTINGS.autoPinColoredTabs; + this.plugin.settings.preventTabDuplication = + DEFAULT_SETTINGS.preventTabDuplication; await this.plugin.saveSettings(); this.plugin.applyAllColors(); this.update(); @@ -376,5 +434,120 @@ class ColorTabSettingTab extends PluginSettingTab { ]; } + // Fallback for Obsidian versions that do not consume getSettingDefinitions. + display() { + const { containerEl } = this; + containerEl.empty(); + + this.plugin.settings.colors.forEach((entry, index) => { + let hexInputEl: HTMLInputElement; + let colorPickerEl: HTMLInputElement; + let swatchEl: HTMLSpanElement; + + const setting = new Setting(containerEl) + .setName(`Color ${index + 1}`) + .addText((text) => { + text.setPlaceholder("Meaning") + .setValue(entry.name) + .onChange(async (value) => { + this.plugin.settings.colors[index].name = value; + await this.plugin.saveSettings(); + }); + text.inputEl.setCssStyles({ width: "120px" }); + text.inputEl.setAttribute("aria-label", "Color meaning / name"); + }) + .addText((hex) => { + hex.setPlaceholder("#rrggbb") + .setValue(entry.color) + .onChange(async (value) => { + const normalized = value.trim(); + if (!/^#[0-9a-fA-F]{6}$/.test(normalized)) return; + this.plugin.settings.colors[index].color = normalized; + await this.plugin.saveSettings(); + this.plugin.applyAllColors(); + if (colorPickerEl) colorPickerEl.value = normalized; + if (swatchEl) swatchEl.style.backgroundColor = normalized; + }); + hex.inputEl.setCssStyles({ width: "88px", fontFamily: "monospace" }); + hex.inputEl.setAttribute("aria-label", "Hex color code"); + hexInputEl = hex.inputEl; + }) + .addColorPicker((picker) => { + picker + .setValue(entry.color) + .onChange(async (value) => { + this.plugin.settings.colors[index].color = value; + await this.plugin.saveSettings(); + this.plugin.applyAllColors(); + if (hexInputEl) hexInputEl.value = value; + if (swatchEl) swatchEl.style.backgroundColor = value; + }); + }); + + swatchEl = setting.controlEl.createEl("span", { + cls: "color-tab-settings-swatch", + }); + swatchEl.style.backgroundColor = entry.color; + + colorPickerEl = setting.controlEl.querySelector( + "input[type=color]" + ) as HTMLInputElement; + + colorPickerEl?.addEventListener("input", () => { + if (hexInputEl) hexInputEl.value = colorPickerEl.value; + if (swatchEl) swatchEl.style.backgroundColor = colorPickerEl.value; + }); + }); + + new Setting(containerEl) + .setName("Auto-pin colored tabs") + .setDesc( + "When enabled, applying a tab color pins the tab and removing color unpins it." + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.autoPinColoredTabs) + .onChange(async (value) => { + this.plugin.settings.autoPinColoredTabs = value; + await this.plugin.saveSettings(); + this.plugin.applyAllColors(); + }); + }); + + new Setting(containerEl) + .setName("Prevent Tab Duplication") + .setDesc( + "When enabled, opening an already-open file will focus on the existing tab instead of creating a duplicate." + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.preventTabDuplication) + .onChange(async (value) => { + this.plugin.settings.preventTabDuplication = value; + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName("Reset to defaults") + .setDesc("Restore the original pastel color palette.") + .addButton((btn) => { + btn.setButtonText("Reset") + .setDestructive() + .onClick(async () => { + this.plugin.settings.colors = DEFAULT_COLORS.map( + (c) => ({ ...c }) + ); + this.plugin.settings.autoPinColoredTabs = + DEFAULT_SETTINGS.autoPinColoredTabs; + this.plugin.settings.preventTabDuplication = + DEFAULT_SETTINGS.preventTabDuplication; + await this.plugin.saveSettings(); + this.plugin.applyAllColors(); + this.display(); + }); + }); + } + } diff --git a/manifest.json b/manifest.json index 777c1a9..096e199 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "color-tab", "name": "Color Tab", - "version": "1.0.4", + "version": "1.1.0", "minAppVersion": "1.13.0", "description": "Colorize open tabs to visually distinguish notes at a glance, using a right-click context menu.", "author": "Rafael Ordaz", diff --git a/package.json b/package.json index e2a65d3..3d71678 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "color-tab", - "version": "1.0.4", + "version": "1.1.0", "description": "Obsidian plugin to colorize open tabs", "main": "main.js", "scripts": {