rordaz_ColorTab/main.ts

307 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
App,
Menu,
Plugin,
PluginSettingTab,
Setting,
WorkspaceLeaf,
} from "obsidian";
interface ColorEntry {
name: string;
color: string;
}
interface ColorTabSettings {
colors: ColorEntry[];
/** Maps file path → hex color */
fileColors: Record<string, string>;
autoPinColoredTabs: boolean;
}
const DEFAULT_COLORS: ColorEntry[] = [
{ name: "Red", color: "#FFB3BA" },
{ name: "Yellow", color: "#FFDFBA" },
{ name: "Green", color: "#B5EAD7" },
{ name: "Blue", color: "#BAE1FF" },
{ name: "Lavender", color: "#E2BAFF" },
];
const DEFAULT_SETTINGS: ColorTabSettings = {
colors: DEFAULT_COLORS,
fileColors: {},
autoPinColoredTabs: true,
};
export default class ColorTabPlugin extends Plugin {
settings!: ColorTabSettings;
async onload() {
await this.loadSettings();
this.addSettingTab(new ColorTabSettingTab(this.app, this));
// Append color options to Obsidian's native tab context menu
this.registerEvent(
this.app.workspace.on(
"file-menu",
(menu, _file, source, leaf) => {
if (source !== "tab-header" || !leaf) return;
this.addColorMenuItems(menu, leaf);
}
)
);
// Re-apply stored colors whenever the layout changes
this.registerEvent(
this.app.workspace.on("layout-change", () =>
this.applyAllColors()
)
);
// 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.onLayoutReady(() => this.applyAllColors());
}
// ── Context menu ──────────────────────────────────────────────────────────
private addColorMenuItems(menu: Menu, leaf: WorkspaceLeaf) {
if (!this.isColorableLeaf(leaf)) return;
menu.addSeparator();
this.settings.colors.forEach(({ name, color }) => {
menu.addItem((item) => {
item.setTitle(name);
item.onClick(() => this.setTabColor(leaf, color));
const el = (item as unknown as { dom: HTMLElement }).dom;
if (el) {
const swatch = el.createEl("span", { cls: "color-tab-swatch" });
swatch.style.setProperty("--swatch-color", color);
}
});
});
menu.addItem((item) => {
item.setTitle("Remove tab color");
item.setIcon("x");
item.onClick(() => this.removeTabColor(leaf));
});
menu.addItem((item) => {
item.setTitle("Remove all tabs' color");
item.setIcon("x-circle");
item.onClick(() => this.removeAllTabColors());
});
}
// ── Color application ─────────────────────────────────────────────────────
setTabColor(leaf: WorkspaceLeaf, color: string) {
if (!this.isColorableLeaf(leaf)) return;
const path = this.getFilePath(leaf);
if (path) {
this.settings.fileColors[path] = color;
this.saveSettings();
}
this.applyColorToLeaf(leaf, color);
if (this.settings.autoPinColoredTabs) {
leaf.setPinned(true);
}
}
removeTabColor(leaf: WorkspaceLeaf) {
if (!this.isColorableLeaf(leaf)) return;
const path = this.getFilePath(leaf);
if (path) {
delete this.settings.fileColors[path];
this.saveSettings();
}
this.applyColorToLeaf(leaf, null);
if (this.settings.autoPinColoredTabs) {
leaf.setPinned(false);
}
}
removeAllTabColors() {
const coloredPaths = new Set(Object.keys(this.settings.fileColors));
this.settings.fileColors = {};
this.saveSettings();
this.app.workspace.iterateAllLeaves((leaf) => {
this.applyColorToLeaf(leaf, null);
const path = this.getFilePath(leaf);
if (
this.settings.autoPinColoredTabs &&
path &&
coloredPaths.has(path)
) {
leaf.setPinned(false);
}
});
}
applyColorToLeaf(leaf: WorkspaceLeaf, color: string | null) {
const tabHeader = (
leaf as unknown as { tabHeaderEl: HTMLElement }
).tabHeaderEl;
if (!tabHeader) return;
if (color) {
tabHeader.style.setProperty("--tab-bg-color", color);
tabHeader.classList.add("color-tab-colored");
} else {
tabHeader.style.removeProperty("--tab-bg-color");
tabHeader.classList.remove("color-tab-colored");
}
}
applyAllColors() {
this.app.workspace.iterateAllLeaves((leaf) => {
const path = this.getFilePath(leaf);
const tabHeader = (
leaf as unknown as { tabHeaderEl?: HTMLElement }
).tabHeaderEl;
const wasAccidentallyColoredNonFileLeaf =
!path && !!tabHeader?.classList.contains("color-tab-colored");
const color = path ? (this.settings.fileColors[path] ?? null) : null;
this.applyColorToLeaf(leaf, color);
if (this.settings.autoPinColoredTabs && color) {
leaf.setPinned(true);
}
if (
this.settings.autoPinColoredTabs &&
wasAccidentallyColoredNonFileLeaf
) {
leaf.setPinned(false);
}
});
}
// ── Helpers ───────────────────────────────────────────────────────────────
private getFilePath(leaf: WorkspaceLeaf): string | null {
const file = (leaf.view as unknown as { file?: { path: string } })
?.file;
return file?.path ?? null;
}
private isColorableLeaf(leaf: WorkspaceLeaf): boolean {
return this.getFilePath(leaf) !== null;
}
// ── Settings persistence ──────────────────────────────────────────────────
async loadSettings() {
const saved = await this.loadData();
this.settings = {
colors: saved?.colors ?? DEFAULT_COLORS.map((c) => ({ ...c })),
fileColors: saved?.fileColors ?? {},
autoPinColoredTabs: saved?.autoPinColoredTabs ?? DEFAULT_SETTINGS.autoPinColoredTabs,
};
}
async saveSettings() {
await this.saveData(this.settings);
}
}
// ── Settings tab ──────────────────────────────────────────────────────────────
class ColorTabSettingTab extends PluginSettingTab {
plugin: ColorTabPlugin;
constructor(app: App, plugin: ColorTabPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "Color Tab Settings" });
containerEl.createEl("p", {
text: "Customize the 5 colors shown in the tab context menu.",
cls: "color-tab-settings-desc",
});
this.plugin.settings.colors.forEach((entry, index) => {
const setting = new Setting(containerEl)
.setName(`Color ${index + 1}`)
.addText((text) => {
text.setPlaceholder("Name")
.setValue(entry.name)
.onChange(async (value) => {
this.plugin.settings.colors[index].name = value;
await this.plugin.saveSettings();
});
text.inputEl.style.width = "140px";
})
.addColorPicker((picker) => {
picker
.setValue(entry.color)
.onChange(async (value) => {
this.plugin.settings.colors[index].color = value;
await this.plugin.saveSettings();
// Re-apply updated colors live
this.plugin.applyAllColors();
});
});
// Live swatch preview next to the controls
const swatch = setting.controlEl.createEl("span", {
cls: "color-tab-settings-swatch",
});
swatch.style.backgroundColor = entry.color;
// Keep swatch in sync when picker changes
const picker = setting.controlEl.querySelector(
"input[type=color]"
) as HTMLInputElement | null;
picker?.addEventListener("input", () => {
swatch.style.backgroundColor = picker.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("Reset to defaults")
.setDesc("Restore the original pastel color palette.")
.addButton((btn) => {
btn.setButtonText("Reset")
.setWarning()
.onClick(async () => {
this.plugin.settings.colors = DEFAULT_COLORS.map(
(c) => ({ ...c })
);
this.plugin.settings.autoPinColoredTabs =
DEFAULT_SETTINGS.autoPinColoredTabs;
await this.plugin.saveSettings();
this.plugin.applyAllColors();
this.display();
});
});
}
}