nix-community / home-manager

Manage a user environment using Nix [maintainer=@rycee]
https://nix-community.github.io/home-manager/
MIT License
7.13k stars 1.84k forks source link

Helper function for adding search engines to FireFox based on Opensearch description XML #5358

Open jennydaman opened 6 months ago

jennydaman commented 6 months ago

Description

Currently there exists the option programs.firefox.profiles..search.engines where search engines can be configured for Firefox. It would be convenient if the search engines could be added from Opensearch description XML.

For example, the search engine at https://search.nixos.org has a <link> element in its <head>

<link rel="search" type="application/opensearchdescription+xml" title="NixOS packages" href="/desc-search-packages.xml">

The document at https://search.nixos.org/desc-search-packages.xml looks like:

<?xml version="1.0"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
                       xmlns:moz="http://www.mozilla.org/2006/browser/search/">
  <ShortName>NixOS packages</ShortName>
  <Description>Search NixOS packages by name or description.</Description>
  <InputEncoding>UTF-8</InputEncoding>
  <Image width="16" height="16" type="image/png">https://nixos.org/favicon.png</Image>
  <Url type="text/html" template="https://search.nixos.org/packages?query={searchTerms}"/>
  <moz:SearchForm>https://search.nixos.org/packages</moz:SearchForm>
</OpenSearchDescription>

This could be translated into the following Nix expression:

programs.firefox.profiles.default.search.engines."NixOS packages" = {
  urls = [{ "https://search.nixos.org/packages?query={searchTerms}" }];
  iconUpdateURL = "https://nixos.org/favicon.png";
  updateInterval = 24 * 60 * 60 * 1000; # every day;
};

I myself am a newcomer to Nix and am not fluent with the Nix language. I was able to write a proof-of-concept in TypeScript:

import * as cheerio from "cheerio";

const URLS = [
  "https://www.startpage.com",
  "https://github.com",
  "https://archlinux.org",
  "https://search.nixos.org"
];

function main() {
  Promise.all(URLS.map(homeManagerFirefoxEngineNixExpressions))
    .then((x) => x.flat(1).forEach(console.log));
}

main();

type OpenSearchDescription = {
  shortName: string,
  description: string,
  inputEncoding: string,
  image: string,
  urlTemplate: string,
};

function fromOpensearchXml(xml: string): OpenSearchDescription {
  const $ = cheerio.load(xml, { xmlMode: true });
  return {
    shortName: $("ShortName").text(),
    description: $("Description").text(),
    inputEncoding: $("InputEncoding").text(),
    image: $("Image").text(),
    urlTemplate: $("Url").attr("template"),
  };
}

function toNixExpression({shortName, image, urlTemplate}: OpenSearchDescription): string {
  return `
"${shortName}" = {
  urls = [{ "${urlTemplate}" }];
  iconUpdateURL = "${image}";
  updateInterval = 24 * 60 * 60 * 1000; # every day;
};
`;
}

async function homeManagerFirefoxEngineNixExpressions(url: string): Promise<string[]> {
  const links = getOpenSearchLinks(url, await fetchBody(url));
  const opensearchXmls = await Promise.all(links.map(fetchBody));
  const descriptions = opensearchXmls.map(fromOpensearchXml);
  return descriptions.map(toNixExpression);
}

async function fetchBody(url: string): Promise<string> {
  const res = await fetch(url, { redirect: "follow" });
  return await res.text();
}

function getOpenSearchLinks(url: string, htmlBody: string): string[] {
  const $ = cheerio.load(htmlBody);
  const selection = $('head link[type="application/opensearchdescription+xml"]');
  return cheerioSelection2Array(selection)
    .map((element) => element.attribs["href"])
    .map((href) => href.startsWith("https://") ? href : joinUrl(url, href));
}

function cheerioSelection2Array<E>($: cheerio.Cheerio<E>): E[] {
  const elements = [];
  for (let i = 0; i < $.length; i++) {
    elements.push($[i]);
  }
  return elements;
}
stale[bot] commented 3 months ago

Thank you for your contribution! I marked this issue as stale due to inactivity. Please be considerate of people watching this issue and receiving notifications before commenting 'I have this issue too'. We welcome additional information that will help resolve this issue. Please read the relevant sections below before commenting.

If you are the original author of the issue

* If this is resolved, please consider closing it so that the maintainers know not to focus on this. * If this might still be an issue, but you are not interested in promoting its resolution, please consider closing it while encouraging others to take over and reopen an issue if they care enough. * If you know how to solve the issue, please consider submitting a Pull Request that addresses this issue.

If you are not the original author of the issue

* If you are also experiencing this issue, please add details of your situation to help with the debugging process. * If you know how to solve the issue, please consider submitting a Pull Request that addresses this issue.

Memorandum on closing issues

Don't be afraid to manually close an issue, even if it holds valuable information. Closed issues stay in the system for people to search, read, cross-reference, or even reopen – nothing is lost! Closing obsolete issues is an important way to help maintainers focus their time and effort.