Use cases
-
You want to change to a different, often used workspace but you cannot, because you have many tabs open with files you are currently working on and you don’t want to lose those (I don’t want to talk about cursor positions of those files as there is a separate Remember Cursor Position plugin that should help you in other cases).
Workaround: you save another (temporary) workspace to save the tabs. -
You just want to refresh the workspace (your main or only workspace), to get rid of the annoying excess ribbon icons or you want to add more sidebar icons of a freshly installed plugin but you can only save the workpace when all the currently open md files have been already dealt with (and can be safely closed), etc.
Plugins needed to install
- Obsidian Advanced URI plugin
- CodeScript Toolkit plugin
With this script you can achieve both, depending which workspace you want to add to the script (currently you can only add one workspace).
import * as obsidian from 'obsidian';
interface SavedTab {
path: string;
viewType: string;
}
let savedTabs: SavedTab[] = [];
// Define the file extensions we want to support
const ATTACHMENT_EXTENSIONS = ['.mp3', '.mp4', '.pdf', '.jpg', '.jpeg', '.gif', '.png'];
const CODE_EXTENSIONS = ['.txt', '.sh', '.bat', '.js', '.jsx', '.ts', '.py'];
const getOpenTabs = (app: obsidian.App): SavedTab[] => {
const results: SavedTab[] = [];
app.workspace.iterateAllLeaves(leaf => {
// Skip sidebar leaves
const isInSidebar = leaf.getRoot().containerEl.classList.contains('mod-left-split') ||
leaf.getRoot().containerEl.classList.contains('mod-right-split');
if (isInSidebar) return;
const viewState = leaf.getViewState();
const viewType = viewState.type;
// Handle markdown files
if (leaf.view instanceof obsidian.MarkdownView && leaf.view.file) {
results.push({
path: leaf.view.file.path,
viewType: 'markdown'
});
}
// Handle attachment files
else if (viewType === 'pdf' || viewType === 'image' || viewType === 'audio' || viewType === 'video') {
if (viewState.state?.file) {
const filePath = viewState.state.file;
const fileExt = '.' + filePath.split('.').pop().toLowerCase();
if (ATTACHMENT_EXTENSIONS.includes(fileExt)) {
console.log(`Found attachment tab: ${filePath} (${viewType})`);
results.push({
path: filePath,
viewType: viewType
});
}
}
}
// Handle code/text files
else if (viewType === 'text' || viewType === 'code') {
if (viewState.state?.file) {
const filePath = viewState.state.file;
const fileExt = '.' + filePath.split('.').pop().toLowerCase();
if (CODE_EXTENSIONS.includes(fileExt)) {
console.log(`Found code tab: ${filePath} (${viewType})`);
results.push({
path: filePath,
viewType: viewType
});
}
}
}
// For partially loaded markdown tabs
else if (viewType === 'markdown' && viewState.state?.file) {
console.log(`Found partially loaded markdown tab: ${viewState.state.file}`);
results.push({
path: viewState.state.file,
viewType: 'markdown'
});
}
});
console.log(`Found ${results.length} tabs:`, results.map(r => `${r.path} (${r.viewType})`));
return results;
};
const reopenTabs = async (app: obsidian.App, tabs: SavedTab[]): Promise<void> => {
if (tabs.length === 0) return;
try {
// Get the main workspace leaf
const mainLeaf = app.workspace.getLeaf();
// Open first file
const firstTab = tabs[0];
const firstFile = app.vault.getAbstractFileByPath(firstTab.path);
if (firstFile instanceof obsidian.TFile) {
console.log(`Opening first file: ${firstTab.path} (${firstTab.viewType})`);
// Open with the correct view type if needed
if (firstTab.viewType !== 'markdown') {
await mainLeaf.openFile(firstFile, { viewMode: firstTab.viewType });
} else {
await mainLeaf.openFile(firstFile);
}
await new Promise(resolve => setTimeout(resolve, 300));
// Open remaining files one by one with delays
for (let i = 1; i < tabs.length; i++) {
const tab = tabs[i];
const file = app.vault.getAbstractFileByPath(tab.path);
if (file instanceof obsidian.TFile) {
console.log(`Opening file ${i}: ${tab.path} (${tab.viewType})`);
const newLeaf = app.workspace.getLeaf('tab');
// Open with the correct view type if needed
if (tab.viewType !== 'markdown') {
await newLeaf.openFile(file, { viewMode: tab.viewType });
} else {
await newLeaf.openFile(file);
}
await new Promise(resolve => setTimeout(resolve, 300));
}
}
}
} catch (error) {
console.error("Error reopening tabs:", error);
}
};
const saveAndReopenTabs = async (app: obsidian.App): Promise<void> => {
try {
// Get all open tabs
savedTabs = getOpenTabs(app);
// Log the number of tabs found, but always continue with the workspace refresh
if (savedTabs.length === 0) {
console.log("No tabs found to save, but still proceeding with workspace refresh");
new obsidian.Notice("No tabs found to save, refreshing workspace");
} else {
console.log(`Saved ${savedTabs.length} tabs before refresh`);
new obsidian.Notice(`Saved ${savedTabs.length} tab(s)`);
}
// Execute workspace refresh command - this will now always run
console.log("Executing workspace refresh...");
window.open("obsidian://advanced-uri?vault=VAULT_NAME&workspace=WORKSPACE_NAME");
// Wait for refresh to complete
console.log("Waiting for refresh to complete...");
await new Promise(resolve => setTimeout(resolve, 2000));
// Only attempt to reopen tabs if we saved some
if (savedTabs.length > 0) {
console.log("Reopening tabs...");
await reopenTabs(app, savedTabs);
new obsidian.Notice(`Reopened ${savedTabs.length} tab(s)`);
}
} catch (error) {
console.error("Error in saveAndReopenTabs:", error);
new obsidian.Notice("Error: Could not save and reopen tabs");
}
};
export class RetrieveAllTabsPlugin extends obsidian.Plugin {
async onload() {
this.addCommand({
id: 'save-and-reopen-tabs',
name: 'Save current tabs and reopen them after workspace refresh',
callback: () => saveAndReopenTabs(this.app)
});
}
}
export async function invoke(app: obsidian.App): Promise<void> {
await saveAndReopenTabs(app);
}
You will need to customize the obsidian://advanced-uri?vault=VAULT_NAME&workspace=WORKSPACE_NAME
line for your own vault and the name of the workspace you have registered before.
Currently, only markdown files and attachments opened by Obsidian can be retrieved. Unfortunately, non-Obsidian files registered through plugins like VSCode Editor, Unitade, etc. are not retrieved. I didn’t dive deeper into this as 99% of the time I have md files only I am working on.
The script could be enhanced with a modal that would allow the user to select the workspace required with buttons or a drop-down, but for me this is enough.
If you want to make 2 of these scripts, you will need to rename save-and-reopen-tabs
id and other name variants of these in the RetrieveAllTabsPlugin and other references.
This is a Typescript file which you will need to save with .ts
extension, e.g. Retrieve-All-Tabs-After-Workspace-Refresh.ts
.
A little guide on how to use these kinds of scripts:
UPD.
26-02-2025
Fixed an issue when the script didn’t execute refresh of workspace when there were no tabs open.