summaryrefslogtreecommitdiff
path: root/search.ha
diff options
context:
space:
mode:
Diffstat (limited to 'search.ha')
-rw-r--r--search.ha280
1 files changed, 280 insertions, 0 deletions
diff --git a/search.ha b/search.ha
new file mode 100644
index 0000000..84f93ec
--- /dev/null
+++ b/search.ha
@@ -0,0 +1,280 @@
+// SPDX-FileCopyrightText: 2025 Matthew Fennell <matthew@fennell.dev>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use dirs;
+use fmt;
+use getopt;
+use io;
+use net::uri;
+use os::exec;
+use os;
+use strings;
+
+type engine = struct {
+ name: str,
+ template: str,
+};
+
+type mode = enum {
+ BROWSER,
+ COPY,
+ STDOUT,
+};
+
+export fn main() void = {
+ const engines = get_engines(config_file_contents());
+ const (mode, name, query) = parsed(os::args, default_engine(), names(engines));
+ const template = match (get_chosen_template(name, engines)) {
+ case let chosen_template: str => yield chosen_template;
+ case => fmt::fatalf("No template found for {}", name);
+ };
+ const link = get_link(query, template);
+ defer free(link);
+ switch (mode) {
+ case mode::BROWSER => browser(link);
+ case mode::COPY => copy(link);
+ case mode::STDOUT => fmt::println(link)!;
+ };
+};
+
+fn names(engines: []engine) []str = {
+ return {
+ let names: []str = [];
+ for (const engine .. engines) {
+ append(names, engine.name);
+ };
+ yield names;
+ };
+};
+
+fn parsed(args: []str, default_engine: str, names: []str) (mode, str, str) = {
+ const cmd = getopt::parse(args, "search selected engine with the given query",
+ ('b', "open browser"),
+ ('c', "copy link"),
+ ('l', "list engines"),
+ "[engine]", "query");
+ defer getopt::finish(&cmd);
+
+ let mode = mode::STDOUT;
+
+ for (const opt .. cmd.opts) {
+ switch (opt.0) {
+ case 'b' => mode = mode::BROWSER;
+ case 'c' => mode = mode::COPY;
+ case 'l' => {
+ for (let name .. names) { fmt::println(name)!; };
+ os::exit(os::status::SUCCESS);
+ };
+ case => fmt::fatal("Unknown option given");
+ };
+ };
+
+ const specified_engine: (str | void) = {
+ const proposed = cmd.args[0];
+ let engine: (str | void) = void;
+ for (let name .. names) {
+ if (proposed == name) {
+ engine = proposed;
+ break;
+ };
+ };
+ yield engine;
+ };
+
+ const (engine, query) = match (specified_engine) {
+ case let specified: str => yield (specified, cmd.args[1..]);
+ case => yield (default_engine, cmd.args);
+ };
+
+ const query = strings::join(" ", query...);
+
+ return (mode, engine, query);
+};
+
+fn default_engine() str = {
+ return "marginalia";
+};
+
+fn config_file_contents() str = {
+ const config_file = dirs::config("search");
+ const config_file = strings::concat(config_file, "/engines.conf");
+ const engines: io::file = os::open(config_file)!;
+ const engines: []u8 = io::drain(engines)!;
+ return strings::fromutf8(engines)!;
+};
+
+fn get_engines(ssv_contents: str) []engine = {
+ const engines: []str = strings::split(ssv_contents, "\n");
+ return {
+ let result: []engine = [];
+ for (let entry: str .. engines) {
+ if (entry == "") {
+ continue;
+ };
+ const slice = strings::split(entry, " ");
+ assert(len(slice) == 2);
+ append(result, engine { name = slice[0], template = slice[1] });
+ };
+ yield result;
+ };
+};
+
+fn get_chosen_template(name: str, engines: []engine) (void | str) = {
+ for (const engine .. engines) {
+ if (engine.name == name) {
+ return engine.template;
+ };
+ };
+};
+
+// Caller must free result
+fn get_link(query: str, template: str) str = {
+ const encoded = encoded(query);
+ const link = strings::replace(template, "%s", encoded);
+ return link;
+};
+
+fn encoded(query: str) str = {
+ const encoded = net::uri::encodequery([("search", query)]);
+ const encoded = strings::cut(encoded, "=").1;
+ return encoded;
+};
+
+fn browser(link: str) void = {
+ const cmd = os::exec::cmd("edbrowse", link);
+ const cmd = match (cmd) {
+ case let cmd: os::exec::command => yield cmd;
+ case => fmt::fatal("Could not start browser");
+ };
+ os::exec::exec(&cmd);
+};
+
+fn copy(link: str) void = {
+ const cmd = os::exec::cmd("wl-copy", "--primary", "--trim-newline", link);
+ const cmd = match (cmd) {
+ case let cmd: os::exec::command => yield cmd;
+ case => fmt::fatal("Could not copy");
+ };
+ os::exec::exec(&cmd);
+};
+
+@test fn should_handle_single_entry() void = {
+ const engines = get_engines("wikipedia https://en.wikipedia.org/wiki/%s");
+ assert(len(engines) == 1);
+ const engine = engines[0];
+ assert(engine.name == "wikipedia");
+ assert(engine.template == "https://en.wikipedia.org/wiki/%s");
+};
+
+@test fn should_handle_multiple_entries() void = {
+ const engines = get_engines("wikipedia https://en.wikipedia.org/wiki/%s\nmarginalia https://old-search.marginalia.nu/search?query=%s&js=no-js");
+ assert(len(engines) == 2);
+ const wikipedia = engines[0];
+ assert(wikipedia.name == "wikipedia");
+ assert(wikipedia.template == "https://en.wikipedia.org/wiki/%s");
+ const marginalia = engines[1];
+ assert(marginalia.name == "marginalia");
+ assert(marginalia.template == "https://old-search.marginalia.nu/search?query=%s&js=no-js");
+};
+
+@test fn should_handle_final_newline() void = {
+ const engines = get_engines("wikipedia https://en.wikipedia.org/wiki/%s\n");
+ assert(len(engines) == 1);
+};
+
+@test fn should_return_none_if_no_engines() void = {
+ const template = get_chosen_template("wikipedia", []);
+ assert(template is void);
+};
+
+@test fn should_return_none_if_engine_not_found() void = {
+ const template = get_chosen_template("wikipedia", [
+ engine {
+ name = "marginalia",
+ template = "https://old-search.marginalia.nu/search?query=%s&js=no-js",
+ }
+ ]);
+ assert(template is void);
+};
+
+@test fn should_return_query_string_if_engine_found() void = {
+ const template = get_chosen_template("wikipedia", [
+ engine {
+ name = "wikipedia",
+ template = "https://en.wikipedia.org/wiki/%s",
+ }
+ ]);
+ assert(template is str);
+ assert(template as str == "https://en.wikipedia.org/wiki/%s");
+};
+
+@test fn space_should_be_encoded() void = {
+ const encoded_query = encoded("hello world");
+ assert(encoded_query == "hello%20world");
+};
+
+@test fn should_handle_equals_sign() void = {
+ const encoded_query = encoded("does 1+1=2?");
+ assert(encoded_query == "does%201+1=2?");
+};
+
+@test fn should_put_query_in_template() void = {
+ const query = "Dune (2021 film)";
+ const template = "https://en.wikipedia.org/wiki/%s";
+ const link = get_link(query, template);
+ defer free(link);
+ assert(link == "https://en.wikipedia.org/wiki/Dune%20(2021%20film)");
+};
+
+@test fn should_separate_name_and_query() void = {
+ const args = ["search", "marginalia", "recipe"];
+ const (_, name, query) = parsed(args, "marginalia", ["marginalia"]);
+ assert(name == "marginalia");
+ assert(query == "recipe");
+};
+
+@test fn query_should_contain_all_remaining_args() void = {
+ const args = ["search", "marginalia", "recipe", "book"];
+ const (_, name, query) = parsed(args, "marginalia", ["marginalia"]);
+ assert(name == "marginalia");
+ assert(query == "recipe book");
+};
+
+@test fn should_use_default_engine_if_none_specified() void = {
+ const args = ["search", "recipe", "book"];
+ const (_, name, query) = parsed(args, "marginalia", ["marginalia", "osm"]);
+ assert(name == "marginalia");
+ assert(query == "recipe book");
+};
+
+@test fn should_default_to_stdout() void = {
+ const (mode, _, _) = parsed(["search", "marginalia", "recipe", "book"], "marginalia", ["marginalia"]);
+ assert(mode == mode::STDOUT);
+};
+
+@test fn can_request_copy() void = {
+ const (mode, _, _) = parsed(["search", "-c", "marginalia", "recipe", "book"], "marginalia", ["marginalia"]);
+ assert(mode == mode::COPY);
+};
+
+@test fn can_request_browser() void = {
+ const (mode, _, _) = parsed(["search", "-b", "marginalia", "recipe", "book"], "marginalia", ["marginalia"]);
+ assert(mode == mode::BROWSER);
+};
+
+@test fn returns_list_of_engine_names() void = {
+ const names = names([
+ engine {
+ name = "marginalia",
+ template = "https://old-search.marginalia.nu/search?query=%s&js=no-js"
+ },
+ engine {
+ name = "wikipedia",
+ template = "https://en.wikipedia.org/wiki/%s"
+ },
+ ]);
+ assert(len(names) == 2);
+ assert(names[0] == "marginalia");
+ assert(names[1] == "wikipedia");
+};