"use strict";
// ==UserScript==
// @name Obsidian Omnisearch in Google CMDS v0.1
// @namespace https://github.com/scambier/userscripts
// @downloadURL https://github.com/scambier/userscripts/raw/master/dist/obsidian-omnisearch-google.user.js
// @updateURL https://github.com/scambier/userscripts/raw/master/dist/obsidian-omnisearch-google.user.js
// @version 0.4.2
// @description Injects Obsidian notes in Google search results with modern design
// @author Simon Cambier, Modified by CMDSPACE
// @match https://google.com/*
// @match https://www.google.com/*
// @icon https://obsidian.md/favicon.ico
// @require https://code.jquery.com/jquery-3.7.1.min.js
// @require https://raw.githubusercontent.com/sizzlemctwizzle/GM_config/master/gm_config.js
// @require https://gist.githubusercontent.com/scambier/109932d45b7592d3decf24194008be4d/raw/9c97aa67ff9c5d56be34a55ad6c18a314e5eb548/waitForKeyElements.js
// @grant GM.xmlHttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM.getValue
// @grant GM.setValue
// ==/UserScript==
/* globals GM_config, jQuery, $, waitForKeyElements */
(function () {
"use strict";
// Google's right "sidebar" that will contain the results div
const sidebarSelector = "#rhs";
// The results div
const resultsDivId = "OmnisearchObsidianResults";
// The "loading"/"no results" label
const loadingSpanId = "OmnisearchObsidianLoading";
// Modern styles for the sidebar panel
const injectStyles = () => {
const style = document.createElement('style');
style.textContent = `
/* Main container styles */
#${resultsDivId} {
margin-top: 20px;
margin-bottom: 20px;
padding: 0;
width: 100%;
min-width: 360px;
box-sizing: border-box;
}
/* Header section */
#${resultsDivId} .obsidian-header {
background: #7C3AED;
color: white;
padding: 16px 20px;
border-radius: 12px 12px 0 0;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid #7C3AED;
border-bottom: none;
}
#${resultsDivId} .obsidian-header-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 16px;
font-weight: 600;
color: white;
}
#${resultsDivId} .obsidian-header svg {
width: 24px;
height: 24px;
filter: brightness(0) invert(1);
}
#${resultsDivId} .obsidian-settings-link {
color: white;
opacity: 0.9;
font-size: 13px;
text-decoration: none;
transition: opacity 0.2s;
}
#${resultsDivId} .obsidian-settings-link:hover {
opacity: 1;
text-decoration: underline;
}
/* Content container */
#${resultsDivId} .obsidian-content {
background: #ffffff;
border: 1px solid #e0e0e0;
border-top: none;
border-radius: 0 0 12px 12px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
/* Result items */
#${resultsDivId} [data-omnisearch-result] {
background: #f8f9fa;
border-radius: 8px;
padding: 14px;
margin-bottom: 12px;
transition: all 0.2s ease;
border: 1px solid transparent;
cursor: pointer;
}
#${resultsDivId} [data-omnisearch-result]:last-child {
margin-bottom: 0;
}
#${resultsDivId} [data-omnisearch-result]:hover {
background: #ffffff;
border-color: #dadce0;
transform: translateX(-2px);
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
/* Result title */
#${resultsDivId} .LC20lb {
color: #1a73e8;
font-size: 15px;
font-weight: 500;
line-height: 1.4;
margin-bottom: 6px;
display: block;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#${resultsDivId} a:hover .LC20lb {
text-decoration: underline;
}
/* Meta information */
#${resultsDivId} .result-meta {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
#${resultsDivId} .result-meta svg {
width: 14px;
height: 14px;
}
#${resultsDivId} .VuuXrf {
color: #7C3AED;
font-size: 12px;
font-weight: 500;
}
#${resultsDivId} .dyjrff {
color: #80868b;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
display: block;
}
/* Excerpt text */
#${resultsDivId} .VwiC3b {
color: #4d5156;
font-size: 13px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Loading state */
#${resultsDivId} #${loadingSpanId} {
display: block;
text-align: center;
color: #5f6368;
padding: 20px;
font-size: 14px;
}
/* Error state */
#${resultsDivId} .error-message {
color: #d93025;
padding: 16px;
text-align: center;
font-size: 14px;
}
#${resultsDivId} .error-message a {
color: #1a73e8;
text-decoration: none;
}
#${resultsDivId} .error-message a:hover {
text-decoration: underline;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
#${resultsDivId} .obsidian-header {
background: #6B2EC5;
color: white;
border-color: #6B2EC5;
}
#${resultsDivId} .obsidian-settings-link {
color: white;
opacity: 0.9;
}
#${resultsDivId} .obsidian-settings-link:hover {
opacity: 1;
}
#${resultsDivId} .obsidian-content {
background: #202124;
border-color: #3c4043;
}
#${resultsDivId} [data-omnisearch-result] {
background: #303134;
}
#${resultsDivId} [data-omnisearch-result]:hover {
background: #3c4043;
border-color: #5f6368;
}
#${resultsDivId} .LC20lb {
color: #8ab4f8;
}
#${resultsDivId} .VuuXrf {
color: #9974F8;
}
#${resultsDivId} .dyjrff {
color: #9aa0a6;
}
#${resultsDivId} .VwiC3b {
color: #bdc1c6;
}
#${resultsDivId} #${loadingSpanId} {
color: #9aa0a6;
}
}
/* Responsive adjustments */
@media (max-width: 1200px) {
#${resultsDivId} .obsidian-header {
padding: 14px 16px;
}
#${resultsDivId} .obsidian-header-title {
font-size: 15px;
}
#${resultsDivId} .obsidian-content {
padding: 12px;
}
}
`;
document.head.appendChild(style);
};
// The `new GM_config()` syntax is not recognized by the TS compiler
// @ts-ignore
const gmc = new GM_config({
id: "ObsidianOmnisearchGoogle",
title: "Omnisearch in Google - Configuration",
fields: {
port: {
label: "HTTP Port",
type: "text",
default: "51361",
},
nbResults: {
label: "Number of results to display",
type: "int",
default: 3,
},
},
events: {
save: () => {
location.reload();
},
init: () => { },
},
});
// Promise resolves when initialization completes
const onInit = (config) => new Promise((resolve) => {
let isInit = () => setTimeout(() => (config.isInit ? resolve() : isInit()), 0);
isInit();
});
// Obsidian logo
const logo = `<svg height="1em" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 256 256">
<style>
.purple { fill: #7C3AED; }
@media (prefers-color-scheme: dark) { .purple { fill: #9974F8; } }
</style>
<path class="purple" d="M94.82 149.44c6.53-1.94 17.13-4.9 29.26-5.71a102.97 102.97 0 0 1-7.64-48.84c1.63-16.51 7.54-30.38 13.25-42.1l3.47-7.14 4.48-9.18c2.35-5 4.08-9.38 4.9-13.56.81-4.07.81-7.64-.2-11.11-1.03-3.47-3.07-7.14-7.15-11.21a17.02 17.02 0 0 0-15.8 3.77l-52.81 47.5a17.12 17.12 0 0 0-5.5 10.2l-4.5 30.18a149.26 149.26 0 0 1 38.24 57.2ZM54.45 106l-1.02 3.06-27.94 62.2a17.33 17.33 0 0 0 3.27 18.96l43.94 45.16a88.7 88.7 0 0 0 8.97-88.5A139.47 139.47 0 0 0 54.45 106Z"/><path class="purple" d="m82.9 240.79 2.34.2c8.26.2 22.33 1.02 33.64 3.06 9.28 1.73 27.73 6.83 42.82 11.21 11.52 3.47 23.45-5.8 25.08-17.73 1.23-8.67 3.57-18.46 7.75-27.53a94.81 94.81 0 0 0-25.9-40.99 56.48 56.48 0 0 0-29.56-13.35 96.55 96.55 0 0 0-40.99 4.79 98.89 98.89 0 0 1-15.29 80.34h.1Z"/><path class="purple" d="M201.87 197.76a574.87 574.87 0 0 0 19.78-31.6 8.67 8.67 0 0 0-.61-9.48 185.58 185.58 0 0 1-21.82-35.9c-5.91-14.16-6.73-36.08-6.83-46.69 0-4.07-1.22-8.05-3.77-11.21l-34.16-43.33c0 1.94-.4 3.87-.81 5.81a76.42 76.42 0 0 1-5.71 15.9l-4.7 9.8-3.36 6.72a111.95 111.95 0 0 0-12.03 38.23 93.9 93.9 0 0 0 8.67 47.92 67.9 67.9 0 0 1 39.56 16.52 99.4 99.4 0 0 1 25.8 37.31Z"/></svg>
`;
function omnisearch() {
const port = gmc.get("port");
const nbResults = gmc.get("nbResults");
// Extract the ?q= part of the URL with URLSearchParams
const params = new URLSearchParams(window.location.search);
const query = params.get("q");
if (!query)
return;
injectLoadingLabel();
GM.xmlHttpRequest({
method: "GET",
url: `http://localhost:${port}/search?q=${query}`,
headers: {
"Content-Type": "application/json",
},
onload: function (res) {
const data = JSON.parse(res.response);
removeLoadingLabel(data.length > 0);
// Keep the x first results
data.splice(nbResults);
const resultsContainer = $(`#${resultsDivId} .obsidian-content`);
// Delete all existing data-omnisearch-result
$("[data-omnisearch-result]").remove();
// Inject results
for (const item of data) {
const url = `obsidian://open?vault=${encodeURIComponent(item.vault)}&file=${encodeURIComponent(item.path)}`;
const element = $(`
<div data-omnisearch-result>
<a href="${url}" style="text-decoration: none; color: inherit;">
<h3 class="LC20lb">${item.basename}</h3>
<div class="result-meta">
${logo}
<span class="VuuXrf">Obsidian</span>
</div>
<div class="dyjrff">${item.path}</div>
<div class="VwiC3b">
${item.excerpt.replaceAll("<br />", " ").replaceAll("<br>", " ")}
</div>
</a>
</div>
`);
resultsContainer.append(element);
}
},
onerror: function (res) {
console.log("Omnisearch error", res);
const span = $("#" + loadingSpanId)[0];
if (span) {
span.parentElement.innerHTML = `
<div class="error-message">
Error: Obsidian is not running or the Omnisearch server is not enabled.
<br /><a href="obsidian://open">Open Obsidian</a>
</div>
`;
}
},
});
}
function injectTitle() {
const id = "OmnisearchObsidianConfig";
if (!$("#" + id)[0]) {
const header = $(`
<div class="obsidian-header">
<div class="obsidian-header-title">
${logo}
<span>Omnisearch Results</span>
</div>
<a id="${id}" class="obsidian-settings-link" href="#">Settings</a>
</div>
`);
$(`#${resultsDivId}`).prepend(header);
$(document).on("click", "#" + id, function (e) {
e.preventDefault();
gmc.open();
});
}
}
function injectResultsContainer() {
const resultsDiv = $(`
<div id="${resultsDivId}">
<div class="obsidian-content"></div>
</div>
`);
$(sidebarSelector).append(resultsDiv); // append instead of prepend to put at bottom
}
function injectLoadingLabel() {
if (!$("#" + loadingSpanId)[0]) {
const label = $(`<span id="${loadingSpanId}">Loading Obsidian results...</span>`);
$(`#${resultsDivId} .obsidian-content`).append(label);
}
}
function removeLoadingLabel(foundResults = true) {
if (foundResults) {
$("#" + loadingSpanId).remove();
} else {
$("#" + loadingSpanId).text("No results found in Obsidian");
}
}
console.log("Loading Omnisearch injector CMDS v0.1");
let init = onInit(gmc);
init.then(() => {
// Inject styles
injectStyles();
// Make sure the results container is there
if (!$(sidebarSelector)[0]) {
// Create sidebar with proper width if it doesn't exist
$("#rcnt").append('<div id="rhs" style="min-width: 400px; flex-shrink: 0;"></div>');
}
injectResultsContainer();
injectTitle();
omnisearch(); // Make an initial call
console.log("Loaded Omnisearch injector CMDS v0.1");
// Keep the results at the bottom of sidebar
waitForKeyElements(sidebarSelector, () => {
// Move to bottom if other elements are added
const omnisearchDiv = $(`#${resultsDivId}`);
if (omnisearchDiv.next().length > 0) {
omnisearchDiv.appendTo(sidebarSelector);
}
});
});
})();