🤖🚫 AI-free content. This post is 100% written by a human, as is everything on my blog. Enjoy!

Scripts to export Safari tabs

May 13, 2025 in Projects

This is a collection (ok, a couple) of Apple automation scripts to extract the tabs out of the topmost Safari window and send them elsewhere.

Pull just the URLs

I use this in a Shortcut to send the URLs to Reeder’s Read it Later.

on run {input, parameters}
	tell application "Safari"
		set tabURLs to {}
		set tabCount to 0
		repeat with t in tabs of window 0
			if name of t is not "Untitled" then
				set tabURLs to tabURLs & (URL of t)
				set tabCount to tabCount + 1
			end if
		end repeat
	end tell

	return tabURLs
end run

Convert tabs into Obsidian

This one uses my pbcopy-chromium tool to construct nodes pastable into Obsidian Canvas.

It’s self-contained, so you can run it with osascript -l JavaScript nodes_to_canvas.js, or embed into a Shortcut.

const PBCOPY_CHROMIUM_PATH = "/usr/bin/pbcopy-chromium";

function simpleHash(str) {
  var hash = 0,
    i,
    chr;
  if (str.length === 0) return hash;
  for (i = 0; i < str.length; i++) {
    chr = str.charCodeAt(i);
    hash = (hash << 5) - hash + chr;
    hash |= 0; // Convert to 32bit integer
  }
  return Math.abs(hash);
}

function getSafariTabs() {
  var safari = Application("Safari");
  var win = safari.windows[0];
  var tabs = win.tabs();
  var tabData = [];
  for (var i = 0; i < tabs.length; i++) {
    var tab = tabs[i];
    if (tab.name() !== "Untitled") {
      tabData.push({
        name: tab.name(),
        url: tab.url(),
      });
    }
  }
  return tabData;
}

function makeCanvasNodes(tabData) {
  const NODE_GAP = 30;
  const NODE_WIDTH = 400;
  const NODE_HEIGHT = 100;
  var nodes = [];
  for (var i = 0; i < tabData.length; i++) {
    var tab = tabData[i];
    nodes.push({
      // Use a stable node ID so in theory you can run this multiple times
      // and avoid duplicates. In theory.
      id: "saved-tab-" + simpleHash(tab.name + tab.url),
      type: "text",
      x: 0,
      y: i * (NODE_HEIGHT + NODE_GAP),
      width: NODE_WIDTH,
      height: NODE_HEIGHT,
      text: `[${tab.name}](${tab.url})`,
    });
  }
  return nodes;
}

function writeToTempFile(app, json) {
  // Write JSON to a temporary file
  var tempFile = app.doShellScript("mktemp");

  // https://stackoverflow.com/a/29133840/6678
  // Apparently normal writing to file has problems with UTF-8
  // This is using the so-called "ObjC bridge"
  str = $.NSString.alloc.initWithUTF8String(json);
  str.writeToFileAtomicallyEncodingError(tempFile, true, $.NSUTF8StringEncoding, null);
  return tempFile;
}

function run(input, parameters) {
  var tabData = getSafariTabs();
  var nodes = makeCanvasNodes(tabData);
  var result = {
    nodes: nodes,
    edges: [],
    center: { x: 0, y: 0 },
  };
  var json = JSON.stringify(result);

  var app = Application.currentApplication();
  app.includeStandardAdditions = true;

  var tempFilePath = writeToTempFile(app, json);

  // Use pbcopy-chromium with the temp file
  app.doShellScript(PBCOPY_CHROMIUM_PATH + " --type=obsidian/canvas < " + tempFilePath);

  // Remove the temp file
  app.doShellScript("rm " + tempFilePath);

  return `${result.nodes.length} Obsidian Canvas nodes copied to clipboard`;
}

Buy me a coffee Liked the post? Treat me to a coffee