This commit is contained in:
Jesse Hines 2026-06-30 17:09:49 -04:00
parent abbb710f93
commit d105532b6a
No known key found for this signature in database
10 changed files with 202 additions and 71 deletions

View file

@ -22,7 +22,6 @@ Open Tab Settings should be compatible with most other plugins, if you encounter
Some known plugin interactions:
- The [PDF++](https://github.com/RyotaUshio/obsidian-pdf-plus) plugin customizes the opening behavior of PDFs which can override Open Tab Setting's behavior. You can make the plugins play nicely together by setting the "How to open..." options to "new tab" in the PDF++ settings.
## Contributing
You can build the plugin with:
```shell
@ -36,4 +35,5 @@ Run them with:
```shell
npm run test
```
This will automatically download the latest Obsidian version and test the plugin against it.
If you'd like to help with translating, add a new locale file under `src/locales`.

View file

@ -2,7 +2,7 @@
"id": "open-tab-settings",
"name": "Open Tab Settings",
"version": "2.1.1",
"minAppVersion": "1.8.3",
"minAppVersion": "1.8.7",
"description": "Adds options to customize how tabs are opened, including open in new tab by default, preventing duplicate tabs, and more.",
"author": "jesse-r-s-hines",
"authorUrl": "https://github.com/jesse-r-s-hines",

43
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "2.1.1",
"license": "MIT",
"dependencies": {
"i18next": "^24.2.1",
"monkey-around": "^3.0.0"
},
"devDependencies": {
@ -385,6 +386,15 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
@ -12703,6 +12713,37 @@
"node": ">=18.18.0"
}
},
"node_modules/i18next": {
"version": "24.2.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz",
"integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.10"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
@ -18459,7 +18500,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

View file

@ -47,6 +47,7 @@
"wdio-obsidian-service": "^3.1.1"
},
"dependencies": {
"i18next": "^24.2.1",
"monkey-around": "^3.0.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 132 KiB

17
src/i18n.ts Normal file
View file

@ -0,0 +1,17 @@
import i18next from 'i18next';
import { getLanguage } from 'obsidian';
// esbuild doesn't have an easy way to include all files in a dir, so we'll list them manually
import en from './locales/en.json';
const locales = {
en,
}
export async function initializeI18n() {
return await i18next.init({
lng: getLanguage(),
fallbackLng: 'en',
resources: Object.fromEntries(Object.entries(locales).map(([k, v]) => [k, {translation: v}])),
interpolation: { escapeValue: false },
});
};

73
src/locales/en.json Normal file
View file

@ -0,0 +1,73 @@
{
"commands": {
"toggle": "Toggle {{name}}",
"enable": "Enable {{name}}",
"disable": "Disable {{name}}",
"cycle": "Cycle {{name}}",
"enabled": "ON",
"disabled": "OFF"
},
"menu": {
"openInSameTab": "Open in same tab",
"openInDuplicateTab": "Open in duplicate tab",
"openInOppositeTabGroup": "Open in opposite tab group"
},
"settings": {
"deduplicateAcrossTabGroups": {
"name": "Deduplicate across tab groups",
"description": "Whether to switch to already open file even if its in a split pane or popout window"
},
"deduplicateTabs": {
"name": "Prevent duplicate tabs",
"description": "If a tab is already open, switch to it instead of re-opening it."
},
"disableOnDevice": {
"name": "Disable on this device",
"description": "When enabled, this plugin does nothing on the current device. This setting is per-device and is not synced. Restart Obsidian or toggle the plugin off and on to apply.",
"notice": "Restart Obsidian for disable on this device to take effect"
},
"focusNewTabs": {
"name": "Focus explicit new tabs",
"description": "Immediately switch to new tabs opened via middle or ctrl click instead of opening them in the background. New tabs created by regular click will always focus regardless."
},
"modClickBehavior": {
"name": "Mod click behavior",
"description": "On ctrl/cmd/middle click open links...",
"options": {
"allow_duplicate": "In duplicate tab",
"no_preview": "Open persistent (non-preview) tab",
"opposite": "In opposite tab group",
"same": "In same tab",
"tab": "In new tab"
}
},
"newTabPlacement": {
"name": "New tab placement",
"description": "Place new tabs...",
"options": {
"after-active": "After active tab",
"after-pinned": "After pinned tabs",
"beginning": "At beginning",
"end": "At end"
}
},
"newTabTabGroupPlacement": {
"name": "New tab tab group placement",
"description": "When the workspace is split, prefer to open new tabs...",
"options": {
"first": "In first tab group",
"last": "In last tab group",
"opposite": "In opposite tab group",
"same": "In same tab group"
}
},
"openInNewTab": {
"name": "Always open in new tab",
"description": "Open files in a new tab by default."
},
"previewTabs": {
"name": "Preview tabs",
"description": "VS Code style preview tabs. Initially open tabs as \"preview\" until interacted with. Preview tabs will be replaced instead of opening in new tab."
}
}
}

View file

@ -8,6 +8,8 @@ import {
isDisabledOnDevice,
} from './settings';
import { TabGroup } from './types';
import { initializeI18n } from './i18n';
import { t } from 'i18next';
/**
@ -30,10 +32,6 @@ function isMainLeaf(leaf: WorkspaceLeaf) {
return (root instanceof WorkspaceRoot || root instanceof WorkspaceFloating);
}
function capitalize(s: string) {
return s.slice(0, 1).toUpperCase() + s.slice(1);
}
/**
* This is a bit hacky, but to support easily changing our settings in Mod click or menu items we're sticking the
* overrides onto the string passed to getLeaf.
@ -66,6 +64,8 @@ export default class OpenTabSettingsPlugin extends Plugin {
settings: OpenTabSettingsPluginSettings = {...DEFAULT_SETTINGS};
async onload() {
await initializeI18n();
await this.loadSettings();
this.addSettingTab(new OpenTabSettingsPluginSettingTab(this.app, this));
@ -77,42 +77,43 @@ export default class OpenTabSettingsPlugin extends Plugin {
this.registerMonkeyPatches();
const commands = [
["openInNewTab", "always open in new tab"],
["deduplicateTabs", "prevent duplicate tabs"],
["openInNewTab", t('settings.openInNewTab.name')],
["deduplicateTabs", t('settings.deduplicateTabs.name')],
] as const;
for (const [setting, name] of commands) {
const id = setting.replace(/[A-Z]/g, l => `-${l.toLowerCase()}`);
this.addCommand({
id: `toggle-${id}`, name: `Toggle ${name}`,
id: `toggle-${id}`, name: t('commands.toggle', { name }),
callback: async () => {
await this.updateSettings({[setting]: !this.settings[setting]});
new Notice(`${capitalize(name)} ${this.settings[setting] ? 'ON' : 'OFF'}`, 2500);
new Notice(`${name}: ` + t(`commands.${this.settings[setting] ? 'enabled' : 'disabled'}`), 2500);
},
});
this.addCommand({
id: `enable-${id}`, name: `Enable ${name}`,
id: `enable-${id}`, name: t('commands.enable', { name }),
callback: async () => {
await this.updateSettings({[setting]: true});
new Notice(`${capitalize(name)} ${this.settings[setting] ? 'ON' : 'OFF'}`, 2500);
new Notice(`${name}: ` + t(`commands.${this.settings[setting] ? 'enabled' : 'disabled'}`), 2500);
},
});
this.addCommand({
id: `disable-${id}`, name: `Disable ${name}`,
id: `disable-${id}`, name: t('commands.disable', { name }),
callback: async () => {
await this.updateSettings({[setting]: false});
new Notice(`${capitalize(name)} ${this.settings[setting] ? 'ON' : 'OFF'}`, 2500);
new Notice(`${name}: ` + t(`commands.${this.settings[setting] ? 'enabled' : 'disabled'}`), 2500);
},
});
}
this.addCommand({
id: `cycle-tab-group-placement`, name: `Cycle tab group placement`,
id: `cycle-tab-group-placement`,
name: t('commands.cycle', {name: t('settings.newTabTabGroupPlacement.name')}),
callback: async () => {
const values = Object.keys(NEW_TAB_TAB_GROUP_PLACEMENTS) as (keyof typeof NEW_TAB_TAB_GROUP_PLACEMENTS)[];
const index = values.findIndex(v => v == this.settings.newTabTabGroupPlacement);
const newValue = values[(index + 1) % values.length];
await this.updateSettings({newTabTabGroupPlacement: newValue});
new Notice(`Tab group placement: ${NEW_TAB_TAB_GROUP_PLACEMENTS[newValue]}`, 2500);
new Notice(`${t('settings.newTabTabGroupPlacement.name')}: ${t(NEW_TAB_TAB_GROUP_PLACEMENTS[newValue])}`, 2500);
},
});
@ -122,7 +123,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
menu.addItem((item) => {
item.setSection("open");
item.setIcon("file-minus")
item.setTitle("Open in same tab");
item.setTitle(t('menu.openInSameTab'));
item.onClick(async () => {
await this.app.workspace.getLeaf(OVERRIDES.same).openFile(file);
});
@ -132,7 +133,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
menu.addItem((item) => {
item.setSection("open");
item.setIcon("files")
item.setTitle("Open in duplicate tab");
item.setTitle(t('menu.openInDuplicateTab'));
item.onClick(async () => {
await this.app.workspace.getLeaf(OVERRIDES.allow_duplicate).openFile(file);
});
@ -143,7 +144,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
menu.addItem((item) => {
item.setSection("open");
item.setIcon("lucide-split-square-horizontal")
item.setTitle("Open in opposite tab group");
item.setTitle(t('menu.openInOppositeTabGroup'));
item.onClick(async () => {
await this.app.workspace.getLeaf(OVERRIDES.opposite).openFile(file);
});

View file

@ -1,26 +1,27 @@
import { App, Notice, PluginSettingTab, Setting } from 'obsidian';
import OpenTabSettingsPlugin from "./main"
import { t } from 'i18next';
export const NEW_TAB_PLACEMENTS = {
"after-active": "After active tab",
"after-pinned": "After pinned tabs",
"beginning": "At beginning",
"end": "At end",
"after-active": 'settings.newTabPlacement.options.after-active',
"after-pinned": 'settings.newTabPlacement.options.after-pinned',
"beginning": 'settings.newTabPlacement.options.beginning',
"end": 'settings.newTabPlacement.options.end',
};
export const NEW_TAB_TAB_GROUP_PLACEMENTS = {
"same": "In same tab group",
"opposite": "In opposite tab group",
"first": "In first tab group",
"last": "In last tab group",
"same": 'settings.newTabTabGroupPlacement.options.same',
"opposite": 'settings.newTabTabGroupPlacement.options.opposite',
"first": 'settings.newTabTabGroupPlacement.options.first',
"last": 'settings.newTabTabGroupPlacement.options.last',
};
export const MOD_CLICK_BEHAVIOR = {
"tab": "In new tab",
"same": "In same tab",
"allow_duplicate": "In duplicate tab",
"opposite": "In opposite tab group",
"no_preview": "Open persistent (non-preview) tab",
"tab": 'settings.modClickBehavior.options.tab',
"same": 'settings.modClickBehavior.options.same',
"allow_duplicate": 'settings.modClickBehavior.options.allow_duplicate',
"opposite": 'settings.modClickBehavior.options.opposite',
"no_preview": 'settings.modClickBehavior.options.no_preview',
}
export interface OpenTabSettingsPluginSettings {
@ -43,6 +44,10 @@ export const DEFAULT_SETTINGS: OpenTabSettingsPluginSettings = {
modClickBehavior: "tab",
}
function translateOptions(options: Record<string, string>): Record<string, string> {
return Object.fromEntries(Object.entries(options).map(([value, label]) => [value, t(label)]));
}
const DISABLED_KEY = "open-tab-settings:disabled-on-device";
export function isDisabledOnDevice(): boolean {
@ -69,8 +74,8 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
this.containerEl.empty();
new Setting(this.containerEl)
.setName('Always open in new tab')
.setDesc('Open files in a new tab by default.')
.setName(t('settings.openInNewTab.name'))
.setDesc(t('settings.openInNewTab.description'))
.addToggle(toggle =>
toggle
.setValue(this.plugin.settings.openInNewTab)
@ -81,8 +86,8 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
);
new Setting(this.containerEl)
.setName('Preview tabs')
.setDesc('VS Code style preview tabs. Initially open tabs as "preview" until interacted with. Preview tabs will be replaced instead of opening in new tab.')
.setName(t('settings.previewTabs.name'))
.setDesc(t('settings.previewTabs.description'))
.addToggle(toggle =>
toggle
.setValue(this.plugin.settings.previewTabs)
@ -96,8 +101,8 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
.setCssStyles({opacity: this.plugin.settings.openInNewTab ? "" : "50%"});
new Setting(this.containerEl)
.setName('Prevent duplicate tabs')
.setDesc('If a tab is already open, switch to it instead of re-opening it.')
.setName(t('settings.deduplicateTabs.name'))
.setDesc(t('settings.deduplicateTabs.description'))
.addToggle(toggle =>
toggle
.setValue(this.plugin.settings.deduplicateTabs)
@ -108,8 +113,8 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
);
new Setting(this.containerEl)
.setName('Deduplicate across tab groups')
.setDesc('Whether to switch to already open file even if its in a split pane or popout window')
.setName(t('settings.deduplicateAcrossTabGroups.name'))
.setDesc(t('settings.deduplicateAcrossTabGroups.description'))
.addToggle(toggle =>
toggle
.setValue(this.plugin.settings.deduplicateAcrossTabGroups)
@ -123,11 +128,8 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
.setCssStyles({opacity: this.plugin.settings.deduplicateTabs ? "" : "50%"});
new Setting(this.containerEl)
.setName('Focus explicit new tabs')
.setDesc(
'Immediately switch to new tabs opened via middle or ctrl click instead of opening them in the ' +
'background. New tabs created by regular click will always focus regardless.'
)
.setName(t('settings.focusNewTabs.name'))
.setDesc(t('settings.focusNewTabs.description'))
// this is just an alias for Obsidian Settings > Editor > Always focus new tabs
.addToggle(toggle =>
toggle
@ -139,11 +141,11 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
);
new Setting(this.containerEl)
.setName('New tab placement')
.setDesc('Place new tabs...')
.setName(t('settings.newTabPlacement.name'))
.setDesc(t('settings.newTabPlacement.description'))
.addDropdown(dropdown =>
dropdown
.addOptions(NEW_TAB_PLACEMENTS)
.addOptions(translateOptions(NEW_TAB_PLACEMENTS))
.setValue(this.plugin.settings.newTabPlacement)
.onChange(async value => {
await this.plugin.updateSettings({
@ -154,11 +156,11 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
)
new Setting(this.containerEl)
.setName('New tab tab group placement')
.setDesc('When the workspace is split, prefer to open new tabs...')
.addDropdown(toggle =>
toggle
.addOptions(NEW_TAB_TAB_GROUP_PLACEMENTS)
.setName(t('settings.newTabTabGroupPlacement.name'))
.setDesc(t('settings.newTabTabGroupPlacement.description'))
.addDropdown(dropdown =>
dropdown
.addOptions(translateOptions(NEW_TAB_TAB_GROUP_PLACEMENTS))
.setValue(this.plugin.settings.newTabTabGroupPlacement)
.onChange(async (value) => {
await this.plugin.updateSettings({
@ -169,19 +171,19 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
);
new Setting(this.containerEl)
.setName('Mod click behavior')
.setDesc('On ctrl/cmd/middle click open links...')
.setName(t('settings.modClickBehavior.name'))
.setDesc(t('settings.modClickBehavior.description'))
.addDropdown(dropdown => {
dropdown.addOption("tab", MOD_CLICK_BEHAVIOR['tab']);
dropdown.addOption("tab", t(MOD_CLICK_BEHAVIOR['tab']));
if (this.plugin.settings.openInNewTab) {
dropdown.addOption("same", MOD_CLICK_BEHAVIOR['same'])
dropdown.addOption("same", t(MOD_CLICK_BEHAVIOR['same']))
}
if (this.plugin.settings.deduplicateTabs) {
dropdown.addOption("allow_duplicate", MOD_CLICK_BEHAVIOR['allow_duplicate'])
dropdown.addOption("allow_duplicate", t(MOD_CLICK_BEHAVIOR['allow_duplicate']))
}
dropdown.addOption("opposite", MOD_CLICK_BEHAVIOR['opposite'])
dropdown.addOption("opposite", t(MOD_CLICK_BEHAVIOR['opposite']))
if (this.plugin.settings.previewTabs) {
dropdown.addOption("no_preview", MOD_CLICK_BEHAVIOR['no_preview'])
dropdown.addOption("no_preview", t(MOD_CLICK_BEHAVIOR['no_preview']))
}
dropdown
.setValue(this.plugin.settings.modClickBehavior)
@ -194,20 +196,14 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
})
new Setting(this.containerEl)
.setName('Disable on this device')
.setDesc(
'When enabled, this plugin does nothing on the current device. ' +
'This setting is per-device and is not synced. Restart Obsidian or toggle the plugin off and on to apply.'
)
.setName(t('settings.disableOnDevice.name'))
.setDesc(t('settings.disableOnDevice.description'))
.addToggle(toggle =>
toggle
.setValue(isDisabledOnDevice())
.onChange((value) => {
setDisabledOnDevice(value);
new Notice(
`Open Tab Settings will be ${value ? 'disabled' : 'enabled'} on this device after restart.`,
5000,
);
new Notice(t('settings.disableOnDevice.notice'), 5000);
this.display();
})
);

View file

@ -9,6 +9,8 @@
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"isolatedModules": true,
"strict": true,
"strictNullChecks": true,