on master help sign in
.gitignorenew file+12
since splitsnapshots
unifiedsplit
@@ -0,0 +1,12 @@
1+frontend/elm-stuff
2+frontend/elm.js
3+zod/base
4+zod/.bin
5+zod/.http.ports
6+zod/.run
7+zod/.urb
8+zod/.vere.lock
9+result
10+result-*
11+frontend/assets/fonts
12+frontend/airlock
INSTRUCTIONS.mdnew file+150
@@ -0,0 +1,150 @@
1+# Installation Instructions
2+
3+You need [Nix](https://nixos.org/download/) with flakes enabled to build the desk, and a ship on Urbit OS 408k to run it.
4+If flakes aren't enabled, add `--extra-experimental-features 'nix-command flakes'` after `nix` in each command.
5+The flake builds on x86_64-linux, aarch64-linux and aarch64-darwin; nixpkgs no longer supports Intel Macs.
6+On an Intel Mac, build the desk on one of those systems and copy the files in `result` over, e.g. with `scp -r` or `cp -RL` (a plain `cp -R` copies only the `result` symlink).
7+
8+## On a fake ship
9+
10+To try %seax out, run it on a fake ~zod. This also needs the Urbit runtime (Vere) 4.4 or newer, or Nix to run it. These steps were tested with Vere 4.6.
11+
12+1. Clone the **seax** project and build the desk. The build ends up in `result`.
13+
14+```bash
15+$ git clone https://github.com/iko-soy/seax
16+$ nix build ./seax
17+```
18+
19+2. Download the v4.6 pill (Urbit OS 408k) and start your fake zod using the pill.
20+
21+```bash
22+$ curl -LO https://bootstrap.urbit.org/urbit-v4.6.pill
23+$ urbit -B urbit-v4.6.pill -F zod --http-port 8080
24+```
25+
26+Without Vere installed, you can run the one from the nixpkgs that the flake pins instead:
27+
28+```bash
29+$ nix run --inputs-from ./seax nixpkgs#urbit -- -B urbit-v4.6.pill -F zod --http-port 8080
30+```
31+
32+On an Intel Mac, download [Vere's macOS x86_64 build](https://bootstrap.urbit.org/vere/live/v4.6/vere-v4.6-macos-x86_64), make it executable with `chmod +x vere-v4.6-macos-x86_64`, and run `./vere-v4.6-macos-x86_64` in place of `urbit`.
33+
34+`--http-port 8080` puts the web interface on port 8080. Without it, Vere tries port 80 first, which works on macOS.
35+If port 8080 is taken, Vere stops; choose another port, and use it in step 6.
36+
37+Booting takes about a minute.
38+The fake ship can't download apps such as Landscape, so it prints errors like `http: fail (..., 504)` and `docket: thread failed`; they don't affect %seax.
39+
40+3. Now, inside dojo run the following:
41+```bash
42+dojo> |new-desk %seax
43+dojo> |mount %seax
44+```
45+
46+4. Back in your shell, in the directory where you ran step 1, replace the mounted desk with the build.
47+Don't do this inside `seax`: its own `zod/seax` holds the desk's source.
48+```bash
49+$ rm -r zod/seax/*
50+$ cp -r result/* zod/seax/
51+$ chmod -R u+w zod/seax
52+```
53+
54+The files Nix builds are read-only, and `chmod` makes the copies writable, so that the next update can replace them.
55+
56+5. In the dojo install the seax agent.
57+```bash
58+dojo> |commit %seax
59+dojo> |install our %seax
60+```
61+
62+6. Go to `http://localhost:8080/apps/seax/` in your browser.
63+Log in with the code that `+code` prints in the dojo.
64+If the page only says "bad session auth", your browser still has a cookie from an earlier fake ship; reload the page.
65+
66+To update the desk later, run `git -C seax pull` and `nix build ./seax`, repeat step 4 and run `|commit %seax`.
67+
68+## On your own ship
69+
70+1. Build the desk as in step 1 above.
71+
72+2. In your ship's dojo, create the desk and mount it. `|mount` puts its files in your pier, at `<pier>/seax`.
73+```bash
74+dojo> |new-desk %seax
75+dojo> |mount %seax
76+```
77+
78+3. Replace the mounted desk with the build:
79+```bash
80+$ rm -r <pier>/seax/*
81+$ cp -r result/* <pier>/seax/
82+$ chmod -R u+w <pier>/seax
83+```
84+
85+4. Install it:
86+```bash
87+dojo> |commit %seax
88+dojo> |install our %seax
89+```
90+
91+5. Open `/apps/seax/` at your ship's web address, or click the %seax tile in Landscape.
92+
93+To update it later, run `git -C seax pull` and `nix build ./seax`, then repeat steps 3 and 4.
94+
95+### Upgrading from the 2023 version
96+
97+The 2023 desk was made for Urbit OS 413k, so it was suspended before your ship could update to 408k.
98+Upgrade it with the steps above, without `|new-desk`: run `|mount %seax` if the desk isn't mounted, then replace its files and run `|commit %seax` and `|install our %seax`.
99+
100+The new agent drops the old searches and closes their subscriptions.
101+It also drops the copy of your Tlon notes, if the old version made one, and the list of peers, and calls off the pings that are still waiting for an answer; see [Peers (disabled)](README.md#peers-disabled).
102+
103+Because the desk dates from before Urbit OS 410k, your ship counts it as essential: a later Urbit OS update waits until %seax supports it, instead of suspending %seax.
104+To have it suspended like other apps, run:
105+```bash
106+dojo> |essential-desk %seax |
107+```
108+`+vats %seax` shows which it is, under `essential desk`.
109+
110+### Removing it
111+
112+- `|uninstall %seax` stops the agent and keeps its data. `/apps/seax/` then answers with an error.
113+- `|nuke %seax` deletes the agent's data. It asks for confirmation first.
114+- `|unmount %seax` deletes `<pier>/seax`. The desk itself stays in your ship, because no command deletes desks.
115+
116+# Development
117+
118+With %seax installed on a running ship (see above), run this in another terminal:
119+```bash
120+$ ./seax/dev/elm-live.sh
121+```
122+
123+This serves the frontend at `http://localhost:8000`, with Elm's debugger, and rebuilds it when the code changes.
124+Requests to `/~/` are passed on to the ship at `http://localhost:8080`; set `SHIP_URL` if your ship is elsewhere.
125+Log in at `http://localhost:8000/~/login` first (reload on "bad session auth"), then open `http://localhost:8000/`.
126+
127+With Nix, the script gets Elm, elm-live, the web font and a patched [airlock](https://github.com/iko-soy/airlock) by itself.
128+Without Nix, you need `elm` 0.19.2 and `elm-live`, and airlock at commit 7e85737 (as pinned in `flake.lock`) in `frontend/airlock`, with `frontend/airlock.patch` applied:
129+```bash
130+$ git clone https://github.com/iko-soy/airlock seax/frontend/airlock
131+$ git -C seax/frontend/airlock checkout 7e85737
132+$ git -C seax/frontend/airlock apply ../airlock.patch
133+```
134+There is no `frontend/assets/fonts/` then, so the browser uses another font wherever the page would use Roboto.
135+
136+To build the desk without Nix, set up airlock as above and run `elm make src/Main.elm --optimize --output elm.js` in `seax/frontend`.
137+Then, in step 4, copy `seax/zod/seax/*` instead of `result/*`, and copy `index.html`, `elm.js`, `script.js`, `fetch-event-source.js` and `assets/` from `seax/frontend` into the mounted desk's `web/` directory.
138+
139+The Nix build pins the Elm packages in `frontend/elm-srcs.nix` and `frontend/registry.dat`.
140+After changing `frontend/elm.json`, regenerate them:
141+```bash
142+$ cd seax/frontend
143+$ nix develop .. --command sh -c 'elm2nix convert > elm-srcs.nix && elm2nix snapshot'
144+```
145+
146+# Usage Instructions
147+
148+Type a query, then press Enter or click "search": %seax collects results from its search sources.
149+On the results page, click a source to show only its results; click more sources to add theirs, and click a source again to remove it.
150+The README's [Usage](README.md#usage) section has more.
LICENSEnew file+7
@@ -0,0 +1,7 @@
1+Copyright 2023 Ilia Kostiuchenko and Evangelos Lamprou
2+
3+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
NOTICEnew file+39
@@ -0,0 +1,39 @@
1+%seax is under the MIT License in LICENSE, except for these files from other
2+projects:
3+
4+- zod/seax/lib/dbug.hoon, zod/seax/lib/server.hoon and the marks in
5+ zod/seax/mar/ other than docket-0.hoon are copied from pkg/base-dev in
6+ urbit/urbit 408k-2 (https://github.com/urbit/urbit).
7+ MIT License, Copyright (c) 2015 Urbit.
8+
9+- zod/seax/lib/docket.hoon, zod/seax/sur/docket.hoon and
10+ zod/seax/mar/docket-0.hoon are copied from pkg/landscape in urbit/urbit
11+ 408k-2. MIT License, Copyright (c) 2015 Urbit.
12+
13+- zod/seax/lib/de-html.hoon is adapted from ++de-xml in
14+ pkg/arvo/sys/zuse.hoon of urbit/urbit, as of July 2023.
15+ MIT License, Copyright (c) 2017 Urbit.
16+
17+- frontend/fetch-event-source.js is the bundle from airlock's example
18+ (https://github.com/iko-soy/airlock), a modified build of
19+ @microsoft/fetch-event-source (https://github.com/Azure/fetch-event-source).
20+ MIT License, Copyright (c) Microsoft Corporation, and
21+ MIT License, Copyright 2023 Ilia Kostiuchenko.
22+
23+- The built frontend includes the Roboto font, which is under the SIL Open
24+ Font License 1.1. Its license is next to it in web/assets/fonts/.
25+
26+- The built frontend's web/elm.js includes compiled code from airlock
27+ (MIT License, Copyright 2023 Ilia Kostiuchenko) and from some of the Elm
28+ packages in frontend/elm.json, all of them under the BSD-3-Clause, MIT or
29+ Apache-2.0 license. elm.json also lists MPL-2.0 packages by ianmackenzie,
30+ such as elm-geometry and elm-3d-scene, but no code from them is compiled
31+ in. Each package's license comes with its source.
32+
33+The MIT License, under the copyright lines above:
34+
35+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
36+
37+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
38+
39+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
README.mdnew file+307
@@ -0,0 +1,307 @@
1+<p align="center">
2+ <img src="frontend/assets/img/logo.png" alt="%seax" width="510">
3+</p>
4+
5+The [Urbit](https://urbit.org/) meta-search engine.
6+
7+<p align="center">
8+ <img src="docs/screenshot.png" alt="%seax showing results for &quot;urbit&quot; from %wiby and %urbit-dev" width="700">
9+</p>
10+
11+## About
12+
13+It's time Urbit got some searching capabilities.
14+
15+For most people, their computer is a bootloader to the closest search engine. Searching is a central part of everyone's computing workflow.
16+
17+The `%seax` meta-search engine is able to aggregate results from open search APIs and bring them inside the comfort of your Urbit ship. No longer do you have to keep a second tab open.
18+
19+Let's make searching fun again. :)
20+
21+## Contents
22+
23+- [Features](#features)
24+- [Search sources](#search-sources)
25+- [Requirements](#requirements)
26+- [Installation](#installation)
27+- [Usage](#usage)
28+- [How it works](#how-it-works)
29+- [Ranking](#ranking)
30+- [Privacy and network traffic](#privacy-and-network-traffic)
31+- [Peers (disabled)](#peers-disabled)
32+- [Debugging](#debugging)
33+- [Adding a search source](#adding-a-search-source)
34+- [Development](#development)
35+- [Project layout](#project-layout)
36+- [Known issues](#known-issues)
37+- [History](#history)
38+- [Credits](#credits)
39+- [License](#license)
40+
41+## Features
42+
43+- One search box for several search engines. Your ship sends the query to all of them and shows their results on one page.
44+- Results appear as each source answers, without waiting for the slowest.
45+- A page that more than one source returns is shown once and ranked higher.
46+- Click a source's name to see only its results.
47+- Runs on your own ship at `/apps/seax/`, with a tile in Landscape. Only you can search: visitors who aren't logged in are sent to the login page.
48+- Nothing runs in the background: %seax sends requests only when you search.
49+
50+## Search sources
51+
52+| Source | Shown as | What it is | Request your ship sends | Status (October 2026) |
53+|---|---|---|---|---|
54+| [wiby](https://wiby.me/) | `%wiby` | "Search Engine for the Classic Web", an index of simple, older-style pages | `GET https://wiby.me/json/?q=<query>`, wiby's public JSON API | Works |
55+| [Urbit docs](https://docs.urbit.org/) | `%urbit-dev` | The site search of docs.urbit.org, which is built on GitBook | `POST https://docs.urbit.org/~gitbook/search` with `{"query": "<query>", "scope": {"mode": "all"}}` | Works |
56+| [CrowdView](https://crowdview.ai/) | `%crowdview` | A search engine for forum discussions (Reddit, Hacker News, Stack Exchange and others) | `GET https://crowdview-next-js.onrender.com/api/search-v3?query=<query>`, the endpoint crowdview.ai's own page uses | Works, but has no results for many queries, such as "urbit" |
57+
58+The query is URL-encoded in the GET requests, and sent as a JSON string to the Urbit docs, whose request also has a `content-type: application/json` header. Your ship adds no user agent, cookies or other headers: Vere sends only `Host`, and `Content-Length` when there's a body.
59+
60+The page's sources row shows each source that is still loading or found something. A source that fails, or has no results, is hidden.
61+
62+Until October 2026, %seax also searched [Yep](https://yep.com/). By then Yep blocked requests that don't come from a browser, so it returned no results, but every search still sent it your query. Its official API needs an API key and is paid after 1,000 requests.
63+
64+## Requirements
65+
66+- A ship running Urbit OS 408k, which needs Vere 4.4 or newer (4.6 is current). The desk declares `[%zuse 408]` in `zod/seax/sys.kelvin`. When your ship moves to a newer Urbit OS, it suspends %seax until you install a version of the desk that declares the new one. A desk upgraded from the 2023 version is the exception; see [Installation](#installation).
67+- To build the desk: [Nix](https://nixos.org/download/) with flakes enabled, on x86_64-linux, aarch64-linux or aarch64-darwin. Nix fetches Elm and the other build tools already built from cache.nixos.org, so the build only compiles the frontend and copies files.
68+
69+## Installation
70+
71+[INSTRUCTIONS.md](INSTRUCTIONS.md) has the full steps: on a fake ship to try %seax out, on your own ship, upgrading the 2023 version, and removing %seax. In short:
72+
73+1. Build the desk: `nix build` in a clone of this repository puts it in `result/`.
74+2. In your ship's dojo, create and mount a desk: `|new-desk %seax`, then `|mount %seax`.
75+3. Replace the files of the mounted desk, `<pier>/seax/`, with those in `result/`. Nix's files are read-only, so make the copies writable with `chmod -R u+w <pier>/seax`, or the next update can't replace them.
76+4. In the dojo: `|commit %seax`, then `|install our %seax`.
77+
78+If you installed the 2023 version, which was made for Urbit OS 413k, it was suspended before your ship could update to 408k. Upgrade it the same way, without `|new-desk`. The new agent drops the old searches, the list of peers, and the copy of your Tlon notes if the old version made one. Because the desk dates from before Urbit OS 410k, your ship counts it as essential: a later Urbit OS update waits until %seax supports it, instead of suspending %seax. Run `|essential-desk %seax |` to have it suspended like other apps.
79+
80+To remove %seax, run `|uninstall %seax`, which stops it and keeps its data. `|nuke %seax` then deletes the data.
81+
82+## Usage
83+
84+Open `/apps/seax/` at your ship's web address, e.g. `http://localhost:8080/apps/seax/`, or click the %seax tile in Landscape. If you aren't logged in, you'll be asked for your `+code` first.
85+
86+- Type a query, then press Enter or click **search**.
87+- The **Sources** row names each source that is still loading or has results. Sources that are loading pulse between grey and black, and can't be clicked until they finish.
88+- Click a source to show only its results. Click more sources to add theirs, and click a source again to remove it. With no source selected, all results are shown. The selection stays when you search again.
89+- Each result shows an emoji, the title and the address. The emoji is picked from a hash of the title and is only decoration. Titles are shown without HTML tags, and with HTML entities and `%` escapes decoded.
90+- Click a title to open the result in a new tab. The address under it isn't a link.
91+- While no source has results, the page says "No results found... :)". It also says so while the sources are still loading.
92+- The logo on the start page tilts as you move the mouse over it.
93+- The page's address doesn't change when you search, so you can't bookmark a search. The back button leaves the app, and reloading the page brings back the start page.
94+- The **[About]** link at the bottom leads to this README, and **[Code]** to this repository.
95+
96+## How it works
97+
98+%seax is a Gall agent, [`zod/seax/app/seax.hoon`](zod/seax/app/seax.hoon), and an Elm app, [`frontend/`](frontend). The agent serves the built Elm app from the desk's `web/` directory.
99+
100+```mermaid
101+sequenceDiagram
102+ participant B as Browser (Elm app)
103+ participant E as Eyre
104+ participant S as %seax
105+ participant I as Iris
106+ participant W as wiby, Urbit docs, CrowdView
107+ B->>E: subscribe to /search/<query> on %seax
108+ E->>S: watch /search/<query>
109+ S-->>B: %flush: every source is loading
110+ S->>I: one HTTP request per source
111+ I->>W: requests from your ship
112+ W-->>I: responses
113+ I-->>S: a response
114+ S-->>B: %drain: what changed in the sources and ranked results
115+```
116+
117+- **Serving the app.** The agent binds `/apps/seax` in Eyre and serves the files in the desk's `web/` directory to logged-in users. Everyone else is redirected to `/~/login`. The Landscape tile comes from `zod/seax/desk.docket-0`.
118+- **Searching.** The Elm app subscribes to `/search/<query>` on the agent through an Eyre channel, using [airlock](https://github.com/iko-soy/airlock). The agent accepts the subscription only from the ship itself. For a query that isn't open yet, it sends one HTTP request per source through Iris. Every new subscriber gets the current state right away.
119+- **Streaming.** Each time a source answers, the agent parses the answer, ranks all the results so far and sends the change. The first message is the whole state (`%flush`), and later ones are diffs of it (`%drain`), made by [`lib/sink.hoon`](zod/seax/lib/sink.hoon) and [`lib/noun-diff.hoon`](zod/seax/lib/noun-diff.hoon). airlock's `Ur.Sub.sink` applies them in the browser.
120+- **Sharing searches.** While a query has subscribers, its results stay in the agent's state. Another tab that searches for the same query gets them right away, without new requests. Queries have to match exactly, including case and spaces. When the last subscriber of a query leaves, the agent forgets the query. A new search in the same tab leaves the old query, but closing or reloading a tab doesn't: Eyre ends the tab's subscriptions when it closes the tab's channel, 12 hours after the tab disconnects. Results aren't refreshed while a query stays open.
121+- **Failures.** A source fails when its response has no body, or the body isn't the JSON the source's parser expects. The agent doesn't look at the HTTP status. When Vere can't make a request (DNS, connection or TLS errors, or a connection that stalls for 5 minutes), Iris returns status 504 with no body, so that source fails too. Iris follows redirects with status 301, 303 and 307; other redirects make the source fail. Failed requests aren't retried.
122+
123+## Ranking
124+
125+Each source has a weight in [`zod/seax/lib/engines.hoon`](zod/seax/lib/engines.hoon), currently `.2` (2.0) for all of them. [`zod/seax/lib/rank.hoon`](zod/seax/lib/rank.hoon) merges the results that have exactly the same URL. Each URL gets a score, which is the product of three numbers:
126+
127+- the weights of the sources that returned it, multiplied together,
128+- the number of times it was returned,
129+- the sum of 1/position for each time, counting positions from 1.
130+
131+Results are shown with the highest score first. For a URL that only one source returns, the score is the source's weight divided by the position: a first result scores 2, a second one 1, a third one 0.67. Since the weights are all the same, the first results of all sources come first, then their second results, and so on. Results with the same score come in no particular order.
132+
133+URLs that differ only by a trailing slash or by `http`/`https` count as different URLs. A merged result keeps the title from one of its sources. See [Known issues](#known-issues) for a bug in the scores of merged results.
134+
135+## Privacy and network traffic
136+
137+- **Searches.** Your query goes from your ship to wiby.me, docs.urbit.org and crowdview-next-js.onrender.com, over HTTPS. Those services see the query and your ship's IP address.
138+- **Nothing else.** %seax makes requests only when you search for a query that isn't already open. It has no timers, and it doesn't contact other ships, except once: an upgrade from an older version calls off that version's pings to its peers that are still waiting for an answer. See [Peers (disabled)](#peers-disabled).
139+- **The page.** It loads nothing from other sites: the fonts, scripts and images all come from your ship. airlock normally pokes ~zod when it opens a channel, and [`frontend/airlock.patch`](frontend/airlock.patch) removes that poke. The bug is reported at [iko-soy/airlock#15](https://github.com/iko-soy/airlock/issues/15). Eyre's login page, which comes with Urbit, loads its fonts from rsms.me and storage.googleapis.com.
140+- **Stored data.** Search results stay in the agent's state while their query is open, which can be up to 12 hours after you close its tab. As with everything your ship does, the requests and responses are also recorded in your pier's event log. The agent also prints your queries and their results in the dojo.
141+- **Other ships.** Only you can search. %seax refuses other ships' pokes and searches, so they can't use it through your ship.
142+
143+## Peers (disabled)
144+
145+From August 2023 until this version, %seax kept a list of peers: other ships that might run %seax. The list started with ~zod and ~racfer-hattes. Every 30 seconds, %seax pinged its peers by subscribing to `/ping` on their %seax, and told them which peers had answered, so that ships running %seax could find each other.
146+
147+Nothing ever used the list, and neither the code nor the commit messages say what it was for. The 2023 code didn't work well either: a peer was dropped after missing one ping, every other check emptied the list, and from October 2023 ships refused the peer lists that other ships sent them, because `+on-poke` couldn't read pokes from other ships.
148+
149+This version disables the peer network:
150+
151+- It doesn't ping other ships or send them anything, and it has no timers.
152+- An upgrade drops the list and calls off the pings that are still waiting for an answer.
153+- It refuses pings and peer lists from other ships.
154+
155+## Debugging
156+
157+%seax prints a lot to the dojo:
158+
159+- the wire of every response it gets from Arvo, e.g. `/search-result/<query>/<source>` for each HTTP response,
160+- each source's parsed results, followed by the state of every source,
161+- the path of every search that closes,
162+- the stack trace of every crash while it handles a response, such as the late answers in [Known issues](#known-issues).
163+
164+A few dojo commands help:
165+
166+- `:seax +dbug` prints the agent's state: each open query, with its number of subscribers and the results of each source. A source that is still loading has no entry, `~` means that it failed, and `[~ ~]` that it found nothing.
167+- `:seax +dbug [%incoming ~]` lists the subscriptions to the agent, i.e. open searches.
168+- `:seax +dbug [%outgoing ~]` lists the agent's own subscriptions. It should print "no matching subscriptions", also right after an upgrade from an older version.
169+- `:seax +dbug [%state '~(key by search-subscriptions)']` evaluates Hoon against the state, here listing the open queries.
170+- `+vats %seax` shows whether the desk is running or suspended, e.g. after an Urbit OS update, and which Urbit OS version it declares in `/sys/kelvin`.
171+
172+When Vere can't make a request, it prints `http: fail (<n>, 504): <reason>`, e.g. `unknown node or service` when a DNS lookup fails, and that source counts as failed.
173+
174+## Adding a search source
175+
176+1. Create `zod/seax/lib/engines/<name>.hoon`. It holds an `engine` core: `++request` builds the HTTP request for a query, and `++results` parses the response into a list of `[title url]`, or `~` if it can't. `++get` in [`zod/seax/sur/seax.hoon`](zod/seax/sur/seax.hoon) makes a GET request with the query appended to a URL. It escapes the query twice, because Vere unescapes the URL once before sending it. For example, for an API that returns `[{"title": ..., "url": ...}, ...]`:
177+
178+ ```hoon
179+ /- *seax
180+ ^- engine
181+ =<
182+ |%
183+ ++ request
184+ |= query=tape
185+ ^- request:http
186+ (get "https://example.com/search?q=" query)
187+ ++ results
188+ |= response=mime-data:iris
189+ ^- (unit (list search-result))
190+ ;< jon=json _biff (de:json:html q.data.response)
191+ (apex jon)
192+ --
193+ |%
194+ ++ apex
195+ =, dejs-soft:format
196+ (ar (ot ~[title+so url+so]))
197+ --
198+ ```
199+
200+2. In [`zod/seax/lib/engines.hoon`](zod/seax/lib/engines.hoon), import it (`/+ engines-<name>`) and add `[name=%<name> engine=engines-<name> weight=.2]` to the list. The weight is a floating-point literal: `.2` is 2.0.
201+3. Build and commit the desk again. Nix only sees files that git tracks, so `git add` the new file before building. The Elm app shows every source the agent has, so it doesn't need any change.
202+
203+A source counts as failed if `++results` returns `~` or an empty list. Note that `ar` from `dejs-soft` gives `~` if even one result doesn't parse.
204+
205+## Development
206+
207+[INSTRUCTIONS.md](INSTRUCTIONS.md#development) explains how to work on the frontend. `dev/elm-live.sh` serves it at `http://localhost:8000` with Elm's debugger, rebuilds it when the code changes, and passes requests for `/~/` on to a running ship.
208+
209+The flake has these outputs:
210+
211+- `packages.<system>.desk`, the default: the desk, with the built frontend in `web/`.
212+- `packages.<system>.frontend`: the built frontend.
213+- `packages.<system>.fonts`: the Roboto Light web font, converted to WOFF2 from nixpkgs' Roboto, and its license.
214+- `packages.<system>.airlock-src`: airlock at the commit pinned in `flake.lock`, with `frontend/airlock.patch` applied.
215+- `devShells.<system>.default`: a shell with `elm` 0.19.2, `elm-live` and `elm2nix`.
216+
217+To change the agent, edit `zod/seax`, rebuild the desk and copy it into your mounted desk, as in the installation steps, then `|commit %seax`. The agent reloads with its state. Nix only sees files that git tracks, so `git add` new files before building.
218+
219+## Project layout
220+
221+```
222+README.md, INSTRUCTIONS.md this file, and how to install and develop
223+LICENSE, NOTICE the MIT License, and the files from other projects
224+flake.nix, flake.lock the Nix build: desk, frontend, font and airlock
225+dev/elm-live.sh frontend development server, with a proxy to a ship
226+docs/screenshot.png the screenshot above
227+zod/seax/ the %seax desk
228+ app/seax.hoon the agent
229+ sur/seax.hoon the search-result and engine types, and ++get
230+ lib/engines.hoon the sources and their weights
231+ lib/engines/*.hoon one file per source
232+ lib/rank.hoon merges and ranks results
233+ lib/sink.hoon sends state as a %flush and then %drain diffs
234+ lib/noun-diff.hoon diffs of nouns, for lib/sink.hoon
235+ lib/dbug.hoon, lib/server.hoon from Urbit's base-dev files
236+ lib/docket.hoon, sur/docket.hoon for the Landscape tile
237+ lib/de-html.hoon, lib/extract-links.hoon, lib/extract-text.hoon, lib/words.hoon
238+ HTML and text helpers from the first, crawler
239+ version, which nothing uses now
240+ mar/ marks for the desk's files
241+ desk.docket-0 the tile: title, description, colour, image, URL,
242+ version, website and license
243+ desk.bill the agent to start: %seax
244+ sys.kelvin [%zuse 408]
245+frontend/ the Elm app
246+ src/Main.elm the whole app
247+ src/Route.elm, src/Pages/Search.elm unused leftovers of an early version;
248+ Pages/Search.elm doesn't compile
249+ index.html, script.js the page, and the glue between Elm and the event stream
250+ fetch-event-source.js event-stream client, from airlock's example
251+ assets/ CSS, the logo and tile images, the logo tilt script
252+ elm.json Elm 0.19.2 and its packages; airlock's code comes from airlock/src
253+ elm-srcs.nix, registry.dat pinned Elm packages, for the Nix build
254+ airlock.patch stops airlock from poking ~zod when it opens a channel
255+```
256+
257+## Known issues
258+
259+The search sources:
260+
261+- **CrowdView often has no results,** and %seax shows a source without results the same way as a failed one: it's hidden.
262+- **One malformed result fails a whole source.** If any result in a source's response doesn't parse, the whole source counts as failed.
263+
264+The agent:
265+
266+- **Scores of merged results are wrong.** `lib/rank.hoon` adds the 1/position terms of a merged URL, and compares scores, as integers instead of floating-point numbers. As a result:
267+ - URLs that 2 sources return always come before URLs that only 1 returns. Among themselves, they come roughly in order of the product of their two positions, smallest first, instead of by their scores, and those whose positions multiply to 4 or less tie at the top.
268+ - URLs that all 3 sources return come before those.
269+ - Single-source results are ranked correctly.
270+- **Late answers crash the agent.** If you search for something else before every source has answered, the late answers to the old query crash the agent's `+on-arvo`, and the dojo prints a stack trace for each one. Nothing else is affected. The agent never cancels its requests.
271+- **Restarting the ship during a search leaves sources loading.** When Vere restarts, Iris cancels the requests that are still open. The agent crashes on each cancellation, and those sources keep loading until the query closes, which can be 12 hours after you close its tab.
272+- **A source that stalls keeps loading** until Vere's HTTP client gives up after 5 minutes.
273+- **Results are never refreshed.** A query that is open in any tab, or in a tab closed less than 12 hours ago, gives the results it already has.
274+
275+The page:
276+
277+- **A `%` in a title can hide it.** Titles are `%`-decoded, and when that fails, as on a lone `%`, the title disappears and the result can't be clicked. Escapes that do decode are decoded even when they are part of the real title.
278+- **Narrow screens are cut off.** The logo and the search box have fixed widths, so on a phone the search box and the search button are cut off, and on the start page the logo too. Titles and addresses never wrap, so long ones are cut off as well. The page can't scroll sideways.
279+- **The source filter stays for the next search.** If the selected sources have no results for the new query, the list is empty without the "No results found" message, and a selected source that is hidden can't be unselected.
280+- **"Come back later" doesn't help.** The no-results message says "We'll try and find you something. Just come back later.", but nothing searches again.
281+- **Searching for the same query again does nothing,** so the page can't refresh its results.
282+- **Searches can't be bookmarked,** because the page's address doesn't change.
283+- **Smaller things:** only the title of a result is a link, although the whole result looks clickable; the logo tilts only on the start page; the page has no favicon; the search box isn't focused when the page opens; and the page logs every event from the ship to the browser console.
284+
285+## History
286+
287+- **July 2023.** iko created the repository on 23 July and started %seax on 27 July, as a web crawler with a reverse word index. On 29 July it switched to searching other search engines, with Yep, CrowdView, wiby and Alexandria as sources, and from 31 July it sent each query to all of them. Streaming results through `%sink` and noun diffs, ranking and the first Elm frontend were in place by 2 August, and filtering by source and animations by 5 August.
288+- **August 2023.** iko added liveness checks between %seax ships on 9 August, and "bad peer discovery" (gossip) on 27 August.
289+- **October 2023.** Evangelos Lamprou added the logo and its tilt effect, improvements to the results page including the emoji, the Urbit developer docs as a source, and the first INSTRUCTIONS.md. He also added a `%get-notes` poke that copied the ship's Tlon notebook notes into the agent's state. The notes were never searched.
290+- **Upstream.** iko also contributed `%sink` and the noun-diff library to urbit/urbit in July 2023 ([#6712](https://github.com/urbit/urbit/pull/6712), [#6681](https://github.com/urbit/urbit/pull/6681)), where they live in `pkg/base-dev`.
291+- **2026.** %seax was ported to Urbit OS 408k:
292+ - It is built with a Nix flake, served from the ship, and has a Landscape tile.
293+ - The docs.urbit.org search replaced the old urbit.org developer search.
294+ - Alexandria, whose API is gone, and the Tlon notes were removed.
295+ - The peer network, which nothing used, was disabled.
296+ - Yep, which blocks requests that don't come from a browser, was dropped.
297+
298+## Credits
299+
300+Created by [~racfer-hattes](https://iko.soy) and [~dopnyr-figbud](https://vagos.lamprou.xyz/).
301+
302+- Results come from [wiby](https://wiby.me/), the [Urbit docs](https://docs.urbit.org/) and [CrowdView](https://crowdview.ai/).
303+- The frontend is written in [Elm](https://elm-lang.org/) with [airlock](https://github.com/iko-soy/airlock), [elm-ui](https://github.com/mdgriffith/elm-ui) and [elm-animator](https://github.com/mdgriffith/elm-animator), and includes the [Roboto](https://github.com/googlefonts/roboto-3-classic) font.
304+
305+## License
306+
307+MIT, see [LICENSE](LICENSE). Files from other projects are listed in [NOTICE](NOTICE).
dev/elm-live.shnew file+24
@@ -0,0 +1,24 @@
1+#!/usr/bin/env bash
2+# Serves the frontend at http://localhost:8000 and rebuilds it on changes.
3+# Requests to /~/ are passed on to the ship at $SHIP_URL, so log in at
4+# http://localhost:8000/~/login first. Extra arguments go to `elm make`.
5+
6+set -e
7+
8+cd "$(dirname "$0")/../frontend"
9+
10+ship=${SHIP_URL:-http://localhost:8080}
11+args=(src/Main.elm --start-page=index.html --proxy-prefix=/~ "--proxy-host=$ship/~" -- --debug --output=elm.js "$@")
12+
13+if command -v nix >/dev/null; then
14+ nix build ..#airlock-src --out-link airlock
15+ nix build ..#fonts --out-link assets/fonts
16+ exec nix develop .. --command elm-live "${args[@]}"
17+elif command -v elm-live >/dev/null; then
18+ if [ ! -d airlock ]; then
19+ echo "Please clone https://github.com/iko-soy/airlock into frontend/airlock and apply frontend/airlock.patch" && exit 1
20+ fi
21+ exec elm-live "${args[@]}"
22+else
23+ echo "Please install either 'nix' or 'elm-live'" && exit 1
24+fi
docs/screenshot.pngnew file

Binary file not shown.

flake.locknew file+45
@@ -0,0 +1,45 @@
1+{
2+ "nodes": {
3+ "airlock": {
4+ "flake": false,
5+ "locked": {
6+ "lastModified": 1692952961,
7+ "narHash": "sha256-xz4esBLKyzcBp0rNkne26lrhIB993gLRJqxg217d8Yk=",
8+ "owner": "iko-soy",
9+ "repo": "airlock",
10+ "rev": "7e857379f10b362a9ee9e271312a4deb1fe65b66",
11+ "type": "github"
12+ },
13+ "original": {
14+ "owner": "iko-soy",
15+ "repo": "airlock",
16+ "rev": "7e857379f10b362a9ee9e271312a4deb1fe65b66",
17+ "type": "github"
18+ }
19+ },
20+ "nixpkgs": {
21+ "locked": {
22+ "lastModified": 1792210252,
23+ "narHash": "sha256-Mh0PUaIv4nFY+8wAGonaEh8iBzwsmdNlwjVHmFg1Biw=",
24+ "owner": "NixOS",
25+ "repo": "nixpkgs",
26+ "rev": "09063bd1f2b36fd9fd793c36dfade7bd40a93dfc",
27+ "type": "github"
28+ },
29+ "original": {
30+ "owner": "NixOS",
31+ "ref": "nixos-unstable",
32+ "repo": "nixpkgs",
33+ "type": "github"
34+ }
35+ },
36+ "root": {
37+ "inputs": {
38+ "airlock": "airlock",
39+ "nixpkgs": "nixpkgs"
40+ }
41+ }
42+ },
43+ "root": "root",
44+ "version": 7
45+}
flake.nixnew file+114
@@ -0,0 +1,114 @@
1+{
2+ description = "seax: an Urbit search agent";
3+
4+ inputs = {
5+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6+ # The Elm library the frontend talks to the ship with.
7+ airlock = {
8+ url = "github:iko-soy/airlock/7e857379f10b362a9ee9e271312a4deb1fe65b66";
9+ flake = false;
10+ };
11+ };
12+
13+ outputs =
14+ {
15+ self,
16+ nixpkgs,
17+ airlock,
18+ }:
19+ let
20+ # nixpkgs 26.11 dropped x86_64-darwin.
21+ forAllSystems = nixpkgs.lib.genAttrs [
22+ "x86_64-linux"
23+ "aarch64-linux"
24+ "aarch64-darwin"
25+ ];
26+ in
27+ {
28+ packages = forAllSystems (
29+ system:
30+ let
31+ pkgs = nixpkgs.legacyPackages.${system};
32+ inherit (self.packages.${system}) airlock-src fonts frontend;
33+ in
34+ {
35+ # The %seax desk, ready to be copied into a mounted desk directory
36+ # and committed with `|commit %seax`. The agent serves the frontend
37+ # from web/ at /apps/seax/.
38+ desk = pkgs.runCommand "seax-desk" { } ''
39+ cp -r --no-preserve=mode ${./zod/seax} $out
40+ cp -r --no-preserve=mode ${frontend} $out/web
41+ '';
42+
43+ frontend = pkgs.stdenv.mkDerivation {
44+ name = "seax-frontend";
45+ src = ./frontend;
46+ nativeBuildInputs = [ pkgs.elmPackages.elm ];
47+ # elm-srcs.nix and registry.dat come from `elm2nix convert` and
48+ # `elm2nix snapshot`; regenerate them when elm.json changes.
49+ configurePhase =
50+ pkgs.elmPackages.fetchElmDeps {
51+ elmPackages = import ./frontend/elm-srcs.nix;
52+ elmVersion = "0.19.2";
53+ registryDat = ./frontend/registry.dat;
54+ }
55+ + ''
56+ # A checkout without .git can still contain dev/elm-live.sh's link.
57+ rm -rf airlock
58+ ln -s ${airlock-src} airlock
59+ '';
60+ buildPhase = ''
61+ elm make src/Main.elm --optimize --output elm.js
62+ '';
63+ installPhase = ''
64+ mkdir -p $out/assets
65+ cp index.html elm.js script.js fetch-event-source.js $out/
66+ cp -r assets/css assets/img assets/js $out/assets/
67+ cp -r ${fonts} $out/assets/fonts
68+ '';
69+ };
70+
71+ # airlock, patched to not poke ~zod when it opens a channel.
72+ airlock-src = pkgs.applyPatches {
73+ name = "airlock";
74+ src = airlock;
75+ patches = [ ./frontend/airlock.patch ];
76+ };
77+
78+ # The web font used by the frontend, served from assets/fonts/.
79+ fonts =
80+ let
81+ robotoLicense = pkgs.fetchurl {
82+ url = "https://raw.githubusercontent.com/googlefonts/roboto-3-classic/v3.016/OFL.txt";
83+ hash = "sha256-BhQCMnqWqtsL+2lKlg7SiezTjTg+OWJDgxq4H+sQnEE=";
84+ };
85+ in
86+ pkgs.runCommand "seax-fonts" { nativeBuildInputs = [ pkgs.woff2 ]; } ''
87+ mkdir -p $out
88+ cp ${pkgs.roboto.src}/web/static/Roboto-Light.ttf roboto-light.ttf
89+ woff2_compress roboto-light.ttf
90+ cp roboto-light.woff2 $out/roboto-light.woff2
91+ cp ${robotoLicense} $out/roboto-license.txt
92+ '';
93+
94+ default = self.packages.${system}.desk;
95+ }
96+ );
97+
98+ devShells = forAllSystems (
99+ system:
100+ let
101+ pkgs = nixpkgs.legacyPackages.${system};
102+ in
103+ {
104+ default = pkgs.mkShell {
105+ packages = [
106+ pkgs.elmPackages.elm
107+ pkgs.elmPackages.elm-live
108+ pkgs.elm2nix
109+ ];
110+ };
111+ }
112+ );
113+ };
114+}
frontend/airlock.patchnew file+16
@@ -0,0 +1,16 @@
1+diff --git a/src/Ur/Run.elm b/src/Ur/Run.elm
2+index 4802e86..fe67d11 100644
3+--- a/src/Ur/Run.elm
4++++ b/src/Ur/Run.elm
5+@@ -312,8 +312,10 @@ update inp msg model =
6+
7+ NeedsActivation ->
8+ let
9++ -- Any PUT creates the channel. An ack of nothing does so
10++ -- without poking ~zod (`ship = ""`) over the network.
11+ ( eventId, reqs ) =
12+- [ Poke { ship = "", agent = "hood", mark = "helm-hi", noun = C.cord "Opening airlock!" } ]
13++ [ Ack 0 ]
14+ |> tag model.eventId
15+ in
16+ ( { model | eventId = eventId }
frontend/assets/css/fonts.cssnew file+10
@@ -0,0 +1,10 @@
1+/* The font file is taken from nixpkgs at build time (`nix build .#fonts`),
2+ see flake.nix. Its license is next to it in ../fonts/. */
3+
4+@font-face {
5+ font-family: 'Roboto';
6+ font-style: normal;
7+ font-weight: 300;
8+ font-display: swap;
9+ src: url('../fonts/roboto-light.woff2') format('woff2');
10+}
frontend/assets/css/main.cssnew file+63
@@ -0,0 +1,63 @@
1+@import url('fonts.css');
2+
3+.button {
4+ border: none;
5+ color: white;
6+ padding: 15px 32px;
7+ text-align: center;
8+ text-decoration: none;
9+ font-size: 16px;
10+ display: inline-block;
11+ margin: 4px 2px;
12+ cursor: pointer;
13+ border-radius: 0;
14+ box-shadow: 0 2px 2px #888888;
15+}
16+
17+.shadow {
18+ shadow: 0 2px 2px #888888;
19+}
20+
21+.result {
22+}
23+
24+.result:hover {
25+ text-decoration: underline;
26+}
27+
28+.search:hover {
29+ text-decoration: underline;
30+}
31+
32+.wavy {
33+ animation-name: wavy;
34+ animation-duration: 4.2s;
35+ animation-timing-function: ease-in-out;
36+ animation-iteration-count: infinite;
37+ position: relative;
38+ top: 0;
39+ left: 0;
40+}
41+@keyframes wavy {
42+ 0% {
43+ top: 0px;
44+ }
45+ 50% {
46+ top: -15px;
47+ }
48+ 100% {
49+ top: 0px;
50+ }
51+}
52+
53+.result-link {
54+ color: #000;
55+ text-decoration: none;
56+ font-size: 12px !important;
57+}
58+
59+.result-title {
60+ color: #000;
61+ text-decoration: none;
62+ font-size: 16px;
63+}
frontend/assets/img/logo.pngnew file

Binary file not shown.

frontend/assets/img/tile.pngnew file

Binary file not shown.

frontend/assets/js/tilt.jsnew file+51
@@ -0,0 +1,51 @@
1+/* Store the element in el */
2+let el = document.getElementById('tilt')
3+
4+/* Get the height and width of the element */
5+const height = el.clientHeight
6+const width = el.clientWidth
7+
8+/* Define function a */
9+function handleMove(e) {
10+
11+ if (el.id != 'tilt') return; /* TODO: Fix this hack. */
12+
13+ /*
14+ * Get position of mouse cursor
15+ * With respect to the element
16+ * On mouseover
17+ */
18+ /* Store the x position */
19+ const xVal = e.layerX
20+ /* Store the y position */
21+ const yVal = e.layerY
22+
23+ /*
24+ * Calculate rotation valuee along the Y-axis
25+ * Here the multiplier 20 is to
26+ * Control the rotation
27+ * You can change the value and see the results
28+ */
29+ const yRotation = 20 * ((xVal - width / 2) / width)
30+
31+ /* Calculate the rotation along the X-axis */
32+ const xRotation = -20 * ((yVal - height / 2) / height)
33+
34+ /* Generate string for CSS transform property */
35+ const string = 'perspective(500px) scale(1.0) rotateX(' + xRotation + 'deg) rotateY(' + yRotation + 'deg)'
36+
37+ /* Apply the calculated transformation */
38+ el.style.transform = string
39+}
40+
41+/* Add listener for mouseout event, remove the rotation */
42+el.addEventListener('mouseout', function() {
43+ el.style.transform = 'perspective(500px) scale(1) rotateX(0) rotateY(0)'
44+})
45+
46+/*
47+ * Add a listener for mousemove event
48+ * Which will trigger function 'handleMove'
49+ * On mousemove
50+ */
51+let mouseMoveEL = el.addEventListener('mousemove', handleMove)
frontend/elm-srcs.nixnew file+267
@@ -0,0 +1,267 @@
1+{
2+
3+ "Evelios/elm-hash" = {
4+ sha256 = "0xiypbcqd8bfv0lzpna6is5a63vf91jy8xa2350asi5y5c5fzrkq";
5+ version = "1.0.0";
6+ };
7+
8+ "TSFoster/elm-bytes-extra" = {
9+ sha256 = "1ip5br0kszhr8kmk19jsqqqhgna89drn3cwb6zn8ncwfrxc8xvs9";
10+ version = "1.3.0";
11+ };
12+
13+ "avh4/elm-color" = {
14+ sha256 = "0n16wnvp87x9az3m5qjrl6smsg7051m719xn5d244painx8xmpzq";
15+ version = "1.0.0";
16+ };
17+
18+ "cmditch/elm-bigint" = {
19+ sha256 = "00wfcvl3kg8l85cyljvgal51qksnqfqkc4g4diljggz6dcl155w2";
20+ version = "2.0.1";
21+ };
22+
23+ "elm-community/list-extra" = {
24+ sha256 = "02grd0p5hc2gvdy4n723d1s28pm1grn95jrzic6jcgb26qh16vcc";
25+ version = "8.7.0";
26+ };
27+
28+ "elm-community/maybe-extra" = {
29+ sha256 = "185jy9jxx3bqf0xl3rmdxfqqmxzcr084llf32glr6hgl5agshywk";
30+ version = "5.3.0";
31+ };
32+
33+ "elm-community/string-extra" = {
34+ sha256 = "014l3lkglaniizwvr5pqbi4z7bb0piq0pp3fdifyd4rdp53ac23f";
35+ version = "4.0.1";
36+ };
37+
38+ "elm/browser" = {
39+ sha256 = "0nagb9ajacxbbg985r4k9h0jadqpp0gp84nm94kcgbr5sf8i9x13";
40+ version = "1.0.2";
41+ };
42+
43+ "elm/bytes" = {
44+ sha256 = "02ywbf52akvxclpxwj9n04jydajcbsbcbsnjs53yjc5lwck3abwj";
45+ version = "1.0.8";
46+ };
47+
48+ "elm/core" = {
49+ sha256 = "19w0iisdd66ywjayyga4kv2p1v9rxzqjaxhckp8ni6n8i0fb2dvf";
50+ version = "1.0.5";
51+ };
52+
53+ "elm/html" = {
54+ sha256 = "1n3gpzmpqqdsldys4ipgyl1zacn0kbpc3g4v3hdpiyfjlgh8bf3k";
55+ version = "1.0.0";
56+ };
57+
58+ "elm/http" = {
59+ sha256 = "008bs76mnp48b4dw8qwjj4fyvzbxvlrl4xpa2qh1gg2kfwyw56v1";
60+ version = "2.0.0";
61+ };
62+
63+ "elm/json" = {
64+ sha256 = "0kjwrz195z84kwywaxhhlnpl3p251qlbm5iz6byd6jky2crmyqyh";
65+ version = "1.1.3";
66+ };
67+
68+ "elm/svg" = {
69+ sha256 = "1cwcj73p61q45wqwgqvrvz3aypjyy3fw732xyxdyj6s256hwkn0k";
70+ version = "1.0.1";
71+ };
72+
73+ "elm/time" = {
74+ sha256 = "0vch7i86vn0x8b850w1p69vplll1bnbkp8s383z7pinyg94cm2z1";
75+ version = "1.0.0";
76+ };
77+
78+ "elm/url" = {
79+ sha256 = "0av8x5syid40sgpl5vd7pry2rq0q4pga28b4yykn9gd9v12rs3l4";
80+ version = "1.0.0";
81+ };
82+
83+ "figbus/elm-urbit-api" = {
84+ sha256 = "1qpb8gv2giqp5rysmh0ar66j341bra6wzqka8qcr6xk82ad6afch";
85+ version = "4.0.1";
86+ };
87+
88+ "hecrj/html-parser" = {
89+ sha256 = "19rr5w9lca7akwyvq1fbi5vrib4x2b9sf1p76dxhf6vmiw71xrxr";
90+ version = "2.4.0";
91+ };
92+
93+ "ianmackenzie/elm-1d-parameter" = {
94+ sha256 = "08r1q80k7w2kyasqk54k1xzd5d1607wj4ahx66jdh7gkrni9m8n5";
95+ version = "1.0.1";
96+ };
97+
98+ "ianmackenzie/elm-3d-camera" = {
99+ sha256 = "1cp7ws6kn4sakzk0hpwy83qyz4mr6xfsk2jb4vbf97wjc1fs1gf7";
100+ version = "3.1.0";
101+ };
102+
103+ "ianmackenzie/elm-3d-scene" = {
104+ sha256 = "0h1yhxdycyayxp31g7dsj2iarhfzb6gls2rbzqydmd1v4px4rafr";
105+ version = "1.0.1";
106+ };
107+
108+ "ianmackenzie/elm-geometry" = {
109+ sha256 = "11vjnr9h1742pn9z55bclsxm5hal1f1v4dd055zd6z14fr91pysn";
110+ version = "3.11.0";
111+ };
112+
113+ "ianmackenzie/elm-triangular-mesh" = {
114+ sha256 = "00n08cc3qj610g2f7f77qpl6b6pwr456czf3prkz6iddmi52h3gp";
115+ version = "1.1.0";
116+ };
117+
118+ "ianmackenzie/elm-units" = {
119+ sha256 = "053nibhpkf906c7g14z48pvinzzfhnrnk5hkj4mjvjnv3r457h97";
120+ version = "2.10.0";
121+ };
122+
123+ "jasonliang-dev/elm-heroicons" = {
124+ sha256 = "038j32fniz3q9kpm7mcjz053fycs2nl7pnishgrrs5djjc6djw1z";
125+ version = "2.0.0";
126+ };
127+
128+ "jxxcarlson/hex" = {
129+ sha256 = "0dd0f2lfny4qwzkqbvixkcgprza2n289y46y1y2mgiay8iisfkx9";
130+ version = "4.0.0";
131+ };
132+
133+ "marcosh/elm-html-to-unicode" = {
134+ sha256 = "1f1b32qkqxl9lqc4g5737kw4yrz32jxcylgbsbmq871jwv4gwdp8";
135+ version = "1.0.4";
136+ };
137+
138+ "mdgriffith/elm-animator" = {
139+ sha256 = "0rbsycykmcjgjlm5r48ycbr9halv2big52ifhs7wpslz0df3hhmh";
140+ version = "1.1.1";
141+ };
142+
143+ "mdgriffith/elm-ui" = {
144+ sha256 = "0ffcqv4a4ad400hwp824m3qq4jy82cqp5ghmhp1m0q7n004z6kgv";
145+ version = "1.1.8";
146+ };
147+
148+ "rtfeldman/elm-css" = {
149+ sha256 = "1gwhgqwclc7clk1ns4qqzyn7b4wvcmccg9qavvb8m694qmwlkzjy";
150+ version = "18.0.0";
151+ };
152+
153+ "rtfeldman/elm-hex" = {
154+ sha256 = "1y0aa16asvwdqmgbskh5iba6psp43lkcjjw9mgzj3gsrg33lp00d";
155+ version = "1.0.0";
156+ };
157+
158+ "toastal/either" = {
159+ sha256 = "1yfgvw81szsipibafnllik65vhki9n1lxdylbyy6fsmj6406g1yv";
160+ version = "3.6.3";
161+ };
162+
163+ "zwilias/elm-html-string" = {
164+ sha256 = "0pfqbva3rawly0cr448lvajgx2lzqq6avb8wgrp67i6sh9zd06f7";
165+ version = "2.0.2";
166+ };
167+
168+ "AdrianRibao/elm-derberos-date" = {
169+ sha256 = "0w4ld89akhhr0zr17vnwcwdiwygmnqdf9nb2g2f3w1b2p3p73acp";
170+ version = "1.2.3";
171+ };
172+
173+ "NoRedInk/elm-string-conversions" = {
174+ sha256 = "0xhjyypf5jmnjih9m3rr2ccyf15i403yvpv5xmknqmhmnnhrkf63";
175+ version = "1.0.1";
176+ };
177+
178+ "bitsoflogic/elm-radixint" = {
179+ sha256 = "0625xbi9bw3xdixhids82gcx7bhmrcd9q0cxpc6rpiy8br7d2lfn";
180+ version = "2.0.0";
181+ };
182+
183+ "bonzaico/murmur3" = {
184+ sha256 = "1ccyaz8n2c9szv25y5pfdp62pdmm81n9h6yhr1r26hab9c15gcxx";
185+ version = "1.0.0";
186+ };
187+
188+ "chain-partners/elm-bignum" = {
189+ sha256 = "17fqqmm83r905zw03gjc0dif3ach4d1aar3cv7dlcn2szp0qpgfs";
190+ version = "1.0.1";
191+ };
192+
193+ "elm-explorations/linear-algebra" = {
194+ sha256 = "19zf3dgrx5bvhybjwq3bp171wzsb4bfk5rrfjv234b2f8q3yi59g";
195+ version = "1.0.3";
196+ };
197+
198+ "elm-explorations/webgl" = {
199+ sha256 = "0ma7laa3ax8zd41s838px1y5s1dsmlqzp4j321lwngxjzvxkm3zy";
200+ version = "1.1.3";
201+ };
202+
203+ "elm/file" = {
204+ sha256 = "1rljcb41dl97myidyjih2yliyzddkr2m7n74x7gg46rcw4jl0ny8";
205+ version = "1.0.5";
206+ };
207+
208+ "elm/parser" = {
209+ sha256 = "0a3cxrvbm7mwg9ykynhp7vjid58zsw03r63qxipxp3z09qks7512";
210+ version = "1.1.0";
211+ };
212+
213+ "elm/random" = {
214+ sha256 = "138n2455wdjwa657w6sjq18wx2r0k60ibpc4frhbqr50sncxrfdl";
215+ version = "1.0.0";
216+ };
217+
218+ "elm/regex" = {
219+ sha256 = "0lijsp50w7n1n57mjg6clpn9phly8vvs07h0qh2rqcs0f1jqvsa2";
220+ version = "1.0.0";
221+ };
222+
223+ "elm/virtual-dom" = {
224+ sha256 = "1yvb8px2z62xd578ag2q0r5hd1vkz9y7dfkx05355iiy1d7jwq4v";
225+ version = "1.0.3";
226+ };
227+
228+ "ianmackenzie/elm-float-extra" = {
229+ sha256 = "0l499d14iabyigdysvx3xa572izmqqki7b1ym2gc7kv4sprrq194";
230+ version = "1.1.0";
231+ };
232+
233+ "ianmackenzie/elm-geometry-linear-algebra-interop" = {
234+ sha256 = "15gjd1h0jv3d35p0hyv4k8z1gpag23dip8yacclajjrnkdxmcpgj";
235+ version = "2.0.2";
236+ };
237+
238+ "ianmackenzie/elm-interval" = {
239+ sha256 = "086x40s9gnl2r7700ff75w0p3wfrxh144ks24ni5cwhirxwwf0pq";
240+ version = "3.1.0";
241+ };
242+
243+ "ianmackenzie/elm-units-interval" = {
244+ sha256 = "0xwb5vj8jcnn88cnwniibgncfvyaxnmwwkrbk1n7ji3aj6w8f8dj";
245+ version = "3.2.0";
246+ };
247+
248+ "ktonon/elm-word" = {
249+ sha256 = "055rs4aacvhl4m25lj70wlk71f4yinwgd74wa4nx36mpj2m743b6";
250+ version = "2.1.2";
251+ };
252+
253+ "myrho/elm-round" = {
254+ sha256 = "0zv0a60wzkx4xib7h07ijcg72mcyb3vb914hk5pjp6rf4k2lv9kj";
255+ version = "1.0.5";
256+ };
257+
258+ "robinheghan/murmur3" = {
259+ sha256 = "15asmgr2zqh7rkywrg5647rpdqkpzxk02v5qc6ndj60jza3gsmjk";
260+ version = "1.0.0";
261+ };
262+
263+ "chelovek0v/bbase64" = {
264+ sha256 = "01fj6lf2xpc3byw89zivh11i6kl5bimlsdjd6m3gsi1v88nfj17d";
265+ version = "1.0.1";
266+ };
267+}
frontend/elm.jsonnew file+72
@@ -0,0 +1,72 @@
1+{
2+ "type": "application",
3+ "source-directories": [
4+ "src",
5+ "airlock/src"
6+ ],
7+ "elm-version": "0.19.2",
8+ "dependencies": {
9+ "direct": {
10+ "Evelios/elm-hash": "1.0.0",
11+ "TSFoster/elm-bytes-extra": "1.3.0",
12+ "avh4/elm-color": "1.0.0",
13+ "cmditch/elm-bigint": "2.0.1",
14+ "elm/browser": "1.0.2",
15+ "elm/bytes": "1.0.8",
16+ "elm/core": "1.0.5",
17+ "elm/html": "1.0.0",
18+ "elm/http": "2.0.0",
19+ "elm/json": "1.1.3",
20+ "elm/svg": "1.0.1",
21+ "elm/time": "1.0.0",
22+ "elm/url": "1.0.0",
23+ "elm-community/list-extra": "8.7.0",
24+ "elm-community/maybe-extra": "5.3.0",
25+ "elm-community/string-extra": "4.0.1",
26+ "figbus/elm-urbit-api": "4.0.1",
27+ "hecrj/html-parser": "2.4.0",
28+ "ianmackenzie/elm-1d-parameter": "1.0.1",
29+ "ianmackenzie/elm-3d-camera": "3.1.0",
30+ "ianmackenzie/elm-3d-scene": "1.0.1",
31+ "ianmackenzie/elm-geometry": "3.11.0",
32+ "ianmackenzie/elm-triangular-mesh": "1.1.0",
33+ "ianmackenzie/elm-units": "2.10.0",
34+ "jasonliang-dev/elm-heroicons": "2.0.0",
35+ "jxxcarlson/hex": "4.0.0",
36+ "marcosh/elm-html-to-unicode": "1.0.4",
37+ "mdgriffith/elm-animator": "1.1.1",
38+ "mdgriffith/elm-ui": "1.1.8",
39+ "rtfeldman/elm-css": "18.0.0",
40+ "rtfeldman/elm-hex": "1.0.0",
41+ "toastal/either": "3.6.3",
42+ "zwilias/elm-html-string": "2.0.2"
43+ },
44+ "indirect": {
45+ "AdrianRibao/elm-derberos-date": "1.2.3",
46+ "NoRedInk/elm-string-conversions": "1.0.1",
47+ "bitsoflogic/elm-radixint": "2.0.0",
48+ "bonzaico/murmur3": "1.0.0",
49+ "chain-partners/elm-bignum": "1.0.1",
50+ "elm/file": "1.0.5",
51+ "elm/parser": "1.1.0",
52+ "elm/random": "1.0.0",
53+ "elm/regex": "1.0.0",
54+ "elm/virtual-dom": "1.0.3",
55+ "elm-explorations/linear-algebra": "1.0.3",
56+ "elm-explorations/webgl": "1.1.3",
57+ "ianmackenzie/elm-float-extra": "1.1.0",
58+ "ianmackenzie/elm-geometry-linear-algebra-interop": "2.0.2",
59+ "ianmackenzie/elm-interval": "3.1.0",
60+ "ianmackenzie/elm-units-interval": "3.2.0",
61+ "ktonon/elm-word": "2.1.2",
62+ "myrho/elm-round": "1.0.5",
63+ "robinheghan/murmur3": "1.0.0"
64+ }
65+ },
66+ "test-dependencies": {
67+ "direct": {},
68+ "indirect": {
69+ "chelovek0v/bbase64": "1.0.1"
70+ }
71+ }
72+}
frontend/fetch-event-source.jsnew file+2
@@ -0,0 +1,2 @@
1+var m=class extends Error{};async function A(r,e,t){let n=r.getReader(),o={done:!1,value:new Uint8Array};for(;o&&!o.done;)o=await Promise.race([n.read(),new Promise((g,i)=>{setTimeout(()=>i(new Error("getBytes timed out")),t)})]),e(o.value)}function R(r){let e,t,n,o=!1;return function(i){e===void 0?(e=i,t=0,n=-1):e=L(e,i);let s=e.length,a=0;for(;t<s;){o&&(e[t]===10&&(a=++t),o=!1);let c=-1;for(;t<s&&c===-1;++t)switch(e[t]){case 58:n===-1&&(n=t-a);break;case 13:o=!0;case 10:c=t;break}if(c===-1)break;r(e.subarray(a,c),n),a=t,n=-1}a===s?e=void 0:a!==0&&(e=e.subarray(a),t-=a)}}function P(r,e,t){let n=k(),o=new TextDecoder;return function(i,s){if(i.length===0)r?.(n),n=k();else if(s>0){let a=o.decode(i.subarray(0,s)),c=s+(i[s+1]===32?2:1),p=o.decode(i.subarray(c));switch(a){case"data":n.data=n.data?n.data+`
2+`+p:p;break;case"event":n.event=p;break;case"id":e?.(n.id=p);break;case"retry":let l=parseInt(p,10);isNaN(l)||t?.(n.retry=l);break}}}}function L(r,e){let t=new Uint8Array(r.length+e.length);return t.set(r),t.set(e,r.length),t}function k(){return{data:"",event:"",id:"",retry:void 0}}var b="text/event-stream",N=1e3,T="last-event-id";function q(r,{signal:e,headers:t,onopen:n,onmessage:o,onclose:g,onerror:i,openWhenHidden:s,fetch:a,responseTimeout:c,...p}){return new Promise((l,M)=>{let y={...t};y.accept||(y.accept=b);let f;function w(){f.abort(),document.hidden||E()}typeof document<"u"&&!s&&document.addEventListener("visibilitychange",w);let S=N,h;function x(){typeof document<"u"&&!s&&document.removeEventListener("visibilitychange",w),clearTimeout(h),f.abort()}e?.addEventListener("abort",()=>{x(),l()});let I=a??fetch,U=n??O,v=!1;async function E(){f=new AbortController;try{let u=await Promise.race([I(r,{...p,headers:y,signal:f.signal}),new Promise((d,C)=>{setTimeout(()=>C(new Error("fetch timed out")),c)})]);if(u.status===404)throw new m("Channel reaped");if(u.status<200||u.status>=300)throw new Error(`Invalid server response: ${u.status}`);await U(u,v),v&&(v=!1),await A(u.body,R(P(o,d=>{d?y[T]=d:delete y[T]},d=>{S=d})),c),g?.(),x(),l()}catch(u){if(!f.signal.aborted)try{v=!0;let d=i?.(u)??S;clearTimeout(h),f.abort(),h=setTimeout(E,d)}catch(d){x(),M(d)}}}E()})}function O(r){let e=r.headers.get("content-type");if(!e?.startsWith(b))throw new Error(`Expected content-type to be ${b}, Actual: ${e}`)}export{b as EventStreamContentType,q as fetchEventSource};
frontend/index.htmlnew file+36
@@ -0,0 +1,36 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <link rel="stylesheet" href="assets/css/main.css" >
6+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+ <style>
8+ body {
9+ margin: 0;
10+ }
11+ @keyframes shimmer {
12+ from {
13+ color: rgb(204, 204, 204)
14+ }
15+ to {
16+ color: rgb(50, 50, 50)
17+ }
18+ }
19+
20+ .shimmer {
21+ animation-duration: 0.6s;
22+ animation-name: shimmer;
23+ animation-iteration-count: infinite;
24+ animation-direction: alternate;
25+ animation-timing-function: ease-in-out;
26+ }
27+ </style>
28+ </head>
29+ <body>
30+ <div id="elm"></div>
31+ <script src="elm.js"></script>
32+ <script type="module" src="script.js"></script>
33+ </body>
34+ <script type="text/javascript" src="assets/js/tilt.js" defer></script>
35+</html>
36+
frontend/registry.datnew file

Binary file not shown.

frontend/script.jsnew file+43
@@ -0,0 +1,43 @@
1+import { fetchEventSource } from "./fetch-event-source.js"
2+
3+// No dots: Eyre would read anything after a dot as a file extension.
4+const uid = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
5+
6+function searchForInit(obj) {
7+ if (obj.init) {
8+ return obj.init;
9+ } else {
10+ for (let key in obj) {
11+ if (obj.hasOwnProperty(key)) {
12+ const result = searchForInit(obj[key]);
13+ if (result) {
14+ return result;
15+ }
16+ }
17+ }
18+ }
19+}
20+
21+let app = searchForInit(Elm)({ node: document.getElementById("elm"), flags: { uid } });
22+
23+app.ports.createEventSource.subscribe((url) => {
24+ fetchEventSource(url, {
25+ headers: {
26+ Accept: 'application/x-urb-jam',
27+ "x-channel-format": 'application/x-urb-jam',
28+ "content-type": 'application/x-urb-jam'
29+ },
30+ credentials: 'include',
31+ responseTimeout: 25000,
32+ openWhenHidden: true,
33+ onmessage(ev) {
34+ console.log(ev)
35+ app.ports.onEventSourceMessage.send({ message: ev.data });
36+ },
37+ onerror(err) {
38+ console.log(err)
39+ app.ports.onEventSourceMessage.send({ error: err });
40+ }
41+ });
42+
43+});
frontend/src/Main.elmnew file+489
@@ -0,0 +1,489 @@
1+port module Main exposing (main)
2+
3+import Animator
4+import Browser.Events exposing (onAnimationFrameDelta)
5+import Dict exposing (Dict)
6+import Element exposing (..)
7+import Element.Background as Background
8+import Element.Border as Border
9+import Element.Events as Events exposing (onClick)
10+import Element.Font as Font
11+import Element.Input as Input
12+import Element.Keyed as Keyed
13+import Html exposing (Html, hr)
14+import Html.Attributes
15+import Html.Parser
16+import Html.String
17+import Html.String
18+import Html.Styled.Attributes exposing (css)
19+import Html.Styled.Attributes exposing (css)
20+import Html.Events exposing (on)
21+import Http
22+import Json.Decode as JD
23+import List
24+import List.Extra
25+import Set exposing (Set)
26+import String.Extra exposing (stripTags)
27+import Time exposing (Posix)
28+import Ur
29+import Ur.Cmd
30+import Ur.Deconstructor as D
31+import Ur.Jam exposing (isSig)
32+import Ur.Run
33+import Ur.Sub
34+import Ur.Types exposing (Noun(..))
35+import Url
36+import ElmEscapeHtml
37+import Json.Encode
38+import Hash
39+
40+{-| The frontend is served by the ship itself, so Eyre is on the same origin.
41+-}
42+url : String
43+url =
44+ ""
45+
46+
47+type SearchEngineState
48+ = Loading
49+ | Failed
50+ | Completed
51+
52+
53+deconstructSearchEngineState : D.Deconstructor SearchEngineState
54+deconstructSearchEngineState =
55+ D.oneOf
56+ [ D.const D.cord "loading" |> D.map (\_ -> Loading)
57+ , D.const D.cord "failed" |> D.map (\_ -> Failed)
58+ , D.const D.cord "completed" |> D.map (\_ -> Completed)
59+ ]
60+
61+
62+type alias Model =
63+ { ship : Maybe String
64+ , search : String
65+ , initiatedSearch : Maybe String
66+ , searchResults : List SearchResult
67+ , resultTimelines : Dict String ( Animator.Timeline Int, Animator.Timeline Bool )
68+ , engines : List ( String, SearchEngineState )
69+ , selectedEngines : Set String
70+ }
71+
72+
73+type alias SearchResult =
74+ { title : String
75+ , link : String
76+ , engines : Set String
77+ }
78+
79+type Msg
80+ = Noop
81+ | GotShipName String
82+ | UpdateSearch String
83+ | Search String
84+ | UpdateSearchResults (List SearchResult) (List ( String, SearchEngineState ))
85+ | OpenResult String
86+ | Tick Posix
87+ | ToggleSearchEngine String
88+
89+
90+main : Ur.Run.Program Model Msg
91+main =
92+ Ur.Run.document
93+ { init =
94+ ( { ship = Nothing
95+ , search = ""
96+ , initiatedSearch = Nothing
97+ , searchResults = []
98+ , resultTimelines = Dict.empty
99+ , engines = []
100+ , selectedEngines = Set.empty
101+ }
102+ , Http.get
103+ { url = url ++ "/~/host"
104+ , expect = Http.expectString (result (always Noop) GotShipName)
105+ }
106+ |> Ur.Cmd.cmd
107+ )
108+ , update = update
109+ , view =
110+ \model ->
111+ { title = "%seax"
112+ , body =
113+ [ view model
114+ |> layout
115+ [ Font.family
116+ [ Font.typeface "monospace"
117+ , Font.typeface "system-ui"
118+ , Font.typeface "-apple-system"
119+ , Font.typeface "BlinkMacSystemFont"
120+ , Font.typeface "Segoe UI"
121+ , Font.typeface "Roboto"
122+ , Font.typeface "Helvetica"
123+ , Font.typeface "Arial"
124+ , Font.sansSerif
125+ ]
126+ , clipX
127+ ]
128+ ]
129+ }
130+ , subscriptions = \model -> Animator.toSubscription Tick model (animator model)
131+ , createEventSource = createEventSource
132+ , urbitSubscriptions =
133+ \{ initiatedSearch, ship } ->
134+ case ( initiatedSearch, ship ) of
135+ ( Just query, Just ship_ ) ->
136+ Ur.Sub.sink
137+ { app = "seax"
138+ , path = [ "search", query ]
139+ , ship = ship_
140+ , deconstructor =
141+ D.cell
142+ (D.list (D.cell D.cord deconstructSearchEngineState))
143+ (D.list (D.cell (D.list D.cord) (D.cell D.cord D.cord)))
144+ |> D.map
145+ (\( engines, results ) ->
146+ UpdateSearchResults
147+ (List.map
148+ (\( resultEngines, ( title, link ) ) ->
149+ { engines = Set.fromList resultEngines, title = title, link = link }
150+ )
151+ results
152+ )
153+ engines
154+ )
155+ }
156+
157+ _ ->
158+ Ur.Sub.none
159+ , onEventSourceMsg = onEventSourceMessage
160+ , urbitUrl = \_ -> url
161+ }
162+
163+
164+logoView : List (Attribute Msg) -> Element Msg
165+logoView attributes =
166+ Element.image (List.append [ Html.Attributes.id "tilt" |> htmlAttribute ] attributes)
167+ { src = "assets/img/logo.png", description = "%seax logo" }
168+
169+noSearchResultsView : Element Msg
170+noSearchResultsView = Element.el
171+ [ Html.Attributes.style "padding" "1em" |> htmlAttribute
172+ , width (px 500)
173+ , Font.size 15
174+ , Html.Attributes.class "wavy" |> htmlAttribute
175+ ]
176+ (Element.text """No results found... :)
177+We'll try and find you something.
178+Just come back later.""")
179+
180+subtleLinkView : String -> String -> Element Msg
181+subtleLinkView text href =
182+ Element.el
183+ [ Font.size 15
184+ , pointer
185+ , mouseOver
186+ [ Font.color (rgb 0.1 0.7 0.7)
187+ ]
188+ ]
189+ (Element.html (Html.a [ Html.Attributes.href href ] [ Html.text text ]))
190+
191+aboutView : Element Msg
192+aboutView = row [ centerX, padding 10, spacing 10]
193+ [ subtleLinkView "[About]" "https://github.com/iko-soy/seax#readme"
194+ , subtleLinkView "[Code]" "https://github.com/iko-soy/seax"
195+ ]
196+
197+titleEmoji : String -> String
198+titleEmoji title =
199+ let maxUnicode = 0x1F3F0
200+ minUnicode = 0x1F300
201+ in title |> Hash.fromString
202+ |> Hash.toString
203+ |> String.toInt
204+ |> Maybe.withDefault 0
205+ |> (\x -> ((modBy (maxUnicode - minUnicode) x) + minUnicode))
206+ |> Char.fromCode
207+ |> String.fromChar
208+ |> (\x -> x ++ " ")
209+
210+
211+view : Model -> Element Msg
212+view model =
213+ case model.initiatedSearch of
214+ Nothing ->
215+ column [ centerX, centerY, padding 8, spacing 16 ]
216+ [ logoView [ centerX, width (px 510), height (px 161) ]
217+ , searchView model
218+ , aboutView
219+ ]
220+
221+ Just _ ->
222+ column [ width fill, spacing 16 ]
223+ [ column [ spacing 16, width (minimum 0 fill), padding 8]
224+ [ row [] [ logoView [height (px 40)], searchView model |> el [] ]
225+ , row [ spacing 8, paddingEach { top = 16, bottom = 16, left = 0, right = 0 } ]
226+ (if List.isEmpty model.engines then
227+ []
228+ else
229+ [ text "Sources: "
230+ , model.engines
231+ |> List.filter (\( _, state ) -> state /= Failed)
232+ |> List.map
233+ (\( name, state ) ->
234+ text ("%" ++ name)
235+ |> el
236+ (Font.bold
237+ :: (if Set.isEmpty model.selectedEngines then
238+ []
239+
240+ else if Set.member name model.selectedEngines then
241+ []
242+
243+ else
244+ [ Font.color (rgb 0.8 0.8 0.8)
245+ ]
246+ )
247+ ++ (case state of
248+ Loading ->
249+ [ Html.Attributes.class "shimmer" |> htmlAttribute
250+ ]
251+
252+ Completed ->
253+ [ mouseOver
254+ [ Font.color (rgb 0.6 0.6 0.6)
255+ ]
256+ , pointer
257+ , Events.onClick (ToggleSearchEngine name)
258+ ]
259+
260+ Failed ->
261+ []
262+ )
263+ )
264+ )
265+ |> wrappedRow
266+ [ spacing 8
267+ , width fill
268+ ]
269+ ]
270+ )
271+ ]
272+ , (if List.isEmpty model.searchResults then
273+ noSearchResultsView
274+ else
275+ Keyed.column [ width (minimum 0 fill), padding 8 ]
276+ (model.searchResults
277+ |> List.filter
278+ (\{ engines } ->
279+ if Set.isEmpty model.selectedEngines then
280+ True
281+
282+ else
283+ Set.intersect engines model.selectedEngines |> Set.isEmpty |> not
284+ )
285+ |> List.map
286+ (\{ title, link } ->
287+ ( link
288+ , column
289+ [ spacing 4
290+ , width (minimum 0 fill)
291+ , pointer
292+ , alpha
293+ (Animator.move
294+ (Dict.get link model.resultTimelines
295+ |> Maybe.map Tuple.second
296+ |> Maybe.withDefault (Animator.init False)
297+ )
298+ (\x ->
299+ if x then
300+ Animator.at 1
301+
302+ else
303+ Animator.at 0
304+ )
305+ )
306+ , Html.Attributes.style "top"
307+ ((Animator.move
308+ (Dict.get link model.resultTimelines
309+ |> Maybe.map Tuple.first
310+ |> Maybe.withDefault (Animator.init 0)
311+ )
312+ (\i -> toFloat i * 0 |> Animator.at)
313+ |> String.fromFloat
314+ )
315+ ++ "px"
316+ )
317+ |> htmlAttribute
318+ , Html.Attributes.style "margin-bottom" "1em" |> htmlAttribute
319+ , Html.Attributes.style "padding" "0.5em" |> htmlAttribute
320+ , Html.Attributes.class "result" |> htmlAttribute
321+ ]
322+ [ (Element.html
323+ (Html.a
324+ [ Html.Attributes.href link
325+ , Html.Attributes.class "result-title"
326+ , Html.Attributes.target "_blank"
327+ ]
328+ [ Html.text
329+ (Maybe.withDefault ""
330+ (Url.percentDecode
331+ (ElmEscapeHtml.unescape
332+ (stripTags
333+ ((titleEmoji title) ++ title)
334+ ))))
335+ ]
336+ )
337+ )
338+ , text link
339+ |> el
340+ [ Font.color (rgb 0.7 0.7 0.7)
341+ , Html.Attributes.class "result-link" |> htmlAttribute
342+ ]
343+ ]
344+ )
345+ )
346+ ))
347+ , Element.html (Html.hr [Html.Attributes.style "width" "95%"] [])
348+ , aboutView
349+ ]
350+
351+
352+onEnter : msg -> Element.Attribute msg
353+onEnter msg =
354+ Element.htmlAttribute
355+ (Html.Events.on "keyup"
356+ (JD.field "key" JD.string
357+ |> JD.andThen
358+ (\key ->
359+ if key == "Enter" then
360+ JD.succeed msg
361+
362+ else
363+ JD.fail "Not the enter key"
364+ )
365+ )
366+ )
367+
368+
369+searchView : { a | search : String } -> Element Msg
370+searchView model =
371+ row [ spacing 8 ]
372+ [ Input.text
373+ [ Html.Attributes.class "shadow" |> htmlAttribute, width (px 400)
374+ , onEnter (Search model.search)
375+ ]
376+ { onChange = UpdateSearch
377+ , placeholder = Nothing
378+ , text = model.search
379+ , label = Input.labelHidden "search"
380+ }
381+ , Input.button [ height fill ]
382+ { label =
383+ text "search"
384+ |> el
385+ [ Html.Attributes.class "search" |> htmlAttribute
386+ , Border.width 2
387+ , padding 6
388+ , Font.size 29
389+ ]
390+ , onPress = Just (Search model.search)
391+ }
392+ ]
393+
394+update : Msg -> Model -> ( Model, Ur.Cmd.Cmd Msg )
395+update msg model =
396+ case msg of
397+ Noop ->
398+ ( model, Ur.Cmd.none )
399+
400+ GotShipName ship ->
401+ ( { model | ship = Just ship }, Ur.Cmd.none )
402+
403+ UpdateSearch search ->
404+ ( { model | search = search }, Ur.Cmd.none )
405+
406+ Search query ->
407+ ( { model | initiatedSearch = Just query }, Ur.Cmd.none )
408+
409+ UpdateSearchResults results engines ->
410+ let
411+ foo =
412+ List.Extra.zip (List.range 0 (List.length results - 1)) results
413+ |> List.map
414+ (\( index, result_ ) ->
415+ ( result_.link
416+ , case Dict.get result_.link model.resultTimelines of
417+ Just timeline ->
418+ timeline |> Tuple.mapFirst (Animator.go Animator.slowly index)
419+
420+ Nothing ->
421+ ( Animator.init index, Animator.init False |> Animator.go Animator.slowly True )
422+ )
423+ )
424+ |> Dict.fromList
425+ in
426+ ( { model
427+ | resultTimelines = foo
428+ , searchResults = results
429+ , engines = engines
430+ }
431+ , Ur.Cmd.none
432+ )
433+
434+ OpenResult url_ ->
435+ ( model, Ur.Cmd.none )
436+
437+ Tick time ->
438+ ( Animator.update time (animator model) model, Ur.Cmd.none )
439+
440+ ToggleSearchEngine name ->
441+ if Set.member name model.selectedEngines then
442+ ( { model | selectedEngines = Set.remove name model.selectedEngines }, Ur.Cmd.none )
443+
444+ else
445+ ( { model | selectedEngines = Set.insert name model.selectedEngines }, Ur.Cmd.none )
446+
447+
448+animator : Model -> Animator.Animator Model
449+animator model =
450+ List.foldl
451+ (\key anim ->
452+ anim
453+ |> Animator.watchingWith
454+ (\m -> Dict.get key m.resultTimelines |> Maybe.map Tuple.first |> Maybe.withDefault (Animator.init 0))
455+ (\index m ->
456+ { m | resultTimelines = m.resultTimelines |> Dict.update key (Maybe.map (\( _, x ) -> ( index, x ))) }
457+ )
458+ (always False)
459+ |> Animator.watchingWith
460+ (\m -> Dict.get key m.resultTimelines |> Maybe.map Tuple.second |> Maybe.withDefault (Animator.init False))
461+ (\shown m ->
462+ { m | resultTimelines = m.resultTimelines |> Dict.update key (Maybe.map (\( x, _ ) -> ( x, shown ))) }
463+ )
464+ (always False)
465+ -- |> Animator.watching
466+ )
467+ Animator.animator
468+ (Dict.keys model.resultTimelines)
469+
470+
471+result : (a -> c) -> (b -> c) -> Result a b -> c
472+result f g res =
473+ case res of
474+ Ok b ->
475+ g b
476+
477+ Err a ->
478+ f a
479+
480+
481+listsHaveElementsInCommon : List comparable -> List comparable -> Bool
482+listsHaveElementsInCommon a b =
483+ Set.intersect (Set.fromList a) (Set.fromList b) |> Set.isEmpty |> not
484+
485+
486+port createEventSource : String -> Cmd msg
487+
488+
489+port onEventSourceMessage : (JD.Value -> msg) -> Sub msg
frontend/src/Pages/Search.elmnew file+149
@@ -0,0 +1,149 @@
1+port module Pages.Search exposing (Model, init, update, view)
2+
3+import Element exposing (..)
4+import Element.Background as Background
5+import Element.Border as Border
6+import Element.Events as Events
7+import Element.Font as Font
8+import Element.Input as Input
9+import Heroicons.Outline
10+import Html.Events
11+import Json.Decode as JD
12+import Svg.Attributes
13+import Ur
14+import Ur.Cmd
15+import Ur.Run
16+import Ur.Sub
17+
18+
19+url : String
20+url =
21+ "http://localhost:8080"
22+
23+
24+type alias Model =
25+ { ship : Maybe String, search : String }
26+
27+
28+type Msg
29+ = Noop
30+ | GotShipName String
31+ | UpdateSearch String
32+ | Search String
33+
34+
35+main : Ur.Run.Program Model Msg
36+main =
37+ Ur.Run.application
38+ { init =
39+ \_ _ ->
40+ ( { ship = Nothing
41+ , search = ""
42+ }
43+ , Cmd.batch
44+ [ Ur.logIn url "lidlut-tabwed-pillex-ridrup"
45+ |> Cmd.map (always Noop)
46+ , Ur.getShipName url |> Cmd.map (result (always Noop) GotShipName)
47+
48+ -- , Ur.scry
49+ -- { url = url
50+ -- , agent = "journal"
51+ -- , path = [ "entries", "all" ]
52+ -- , error = Noop
53+ -- , success =
54+ -- D.cell D.ignore
55+ -- (D.cell (D.const D.cord "jrnl")
56+ -- (D.list (D.cell D.bigint D.cord))
57+ -- )
58+ -- |> D.map (\( (), ( (), listings ) ) -> GotListings listings)
59+ -- }
60+ ]
61+ |> Ur.Cmd.cmd
62+ )
63+ , update = update
64+ , view = \model -> { title = "%seax", body = [ view model |> layout [] ] }
65+ , subscriptions = always Sub.none
66+ , createEventSource = createEventSource
67+ , urbitSubscriptions = always Ur.Sub.none
68+
69+ -- \{ entries, shipName } ->
70+ -- case ( entries, shipName ) of
71+ -- ( Just _, Just ship ) ->
72+ -- Ur.Sub.subscribe
73+ -- { ship = ship
74+ -- , app = "journal"
75+ -- , path = [ "updates" ]
76+ -- , deconstructor = decodeJournalUpdate |> D.map GotUpdate
77+ -- }
78+ -- _ ->
79+ -- Ur.Sub.none
80+ , onEventSourceMsg = onEventSourceMessage
81+ , onUrlChange = \_ -> Noop
82+ , onUrlRequest = \_ -> Noop
83+ , urbitUrl = \_ -> url
84+ }
85+
86+
87+view : Model -> Element Msg
88+view model =
89+ column [ centerX, centerY ]
90+ [ el [ centerX, Font.size 67, Font.heavy ] (text "%seax")
91+ , row [ padding 16, spacing 8 ]
92+ [ Input.text []
93+ { onChange = UpdateSearch
94+ , placeholder = Nothing
95+ , text = model.search
96+ , label = Input.labelHidden "search"
97+ }
98+ , Input.button [ height fill ]
99+ { label =
100+ text "search"
101+ |> el
102+ [ Border.width 4
103+ , padding 4
104+ , Font.size 29
105+ , Font.bold
106+ , Border.color (rgb 0 0 0)
107+ , Background.color (rgb 0 0 0)
108+ , Font.color (rgb 1 1 1)
109+ , mouseOver
110+ [ Font.color (rgb 0 0 0)
111+ , Background.color (rgb 1 1 1)
112+ ]
113+ ]
114+ , onPress = Just (Search model.search)
115+ }
116+ ]
117+ ]
118+
119+
120+update : Msg -> Model -> ( Model, Ur.Cmd.Cmd Msg )
121+update msg model =
122+ case msg of
123+ Noop ->
124+ ( model, Ur.Cmd.none )
125+
126+ GotShipName ship ->
127+ ( { model | ship = Just ship }, Ur.Cmd.none )
128+
129+ UpdateSearch search ->
130+ ( { model | search = search }, Ur.Cmd.none )
131+
132+ Search query ->
133+ ( model, Ur.Cmd.none )
134+
135+
136+result : (a -> c) -> (b -> c) -> Result a b -> c
137+result f g res =
138+ case res of
139+ Ok b ->
140+ g b
141+
142+ Err a ->
143+ f a
144+
145+
146+port createEventSource : String -> Cmd msg
147+
148+
149+port onEventSourceMessage : (JD.Value -> msg) -> Sub msg
frontend/src/Route.elmnew file+28
@@ -0,0 +1,28 @@
1+module Route exposing (..)
2+
3+import Url.Builder as Builder
4+import Url.Parser exposing ((</>), (<?>), Parser, map, oneOf, s, string, top)
5+import Url.Parser.Query as Query
6+
7+
8+type Route
9+ = SearchRoute
10+ | ResultsRoute String
11+
12+
13+parser : Parser (Route -> a) a
14+parser =
15+ oneOf
16+ [ map ResultsRoute (s "search" </> string)
17+ , map SearchRoute top
18+ ]
19+
20+
21+printer : Route -> String
22+printer route =
23+ case route of
24+ SearchRoute ->
25+ Builder.absolute [] []
26+
27+ ResultsRoute query ->
28+ Builder.absolute [ "search", query ] []
zod/seax/app/seax.hoon+131−40
@@ -1,6 +1,5 @@
1/- *seax
1/- *seax
2−/+ dbug, engines, rank, sink
2+/+ dbug, engines, rank, server, sink
3−
4|%
3|%
5+$ query cord
4+$ query cord
6+$ search-state
5+$ search-state
@@ -8,14 +7,16 @@ $:
8 number-of-subscriptions=@u
7 number-of-subscriptions=@u
9 search-results=(map term (unit (list search-result)))
8 search-results=(map term (unit (list search-result)))
10==
9==
10+:: state-0 (2023), and state-1 and state-2 (only in development builds of
11+:: the 408k port), also kept peers: other ships that may run %seax. They
12+:: were pinged and told about each other, but never searched, so they are
13+:: dropped on load. state-0 also kept notes from Tlon's %diary agent.
14+::
11−+$ state-0
15++$ state-3
12$:
16$:
17+ %3
13 search-subscriptions=(map query search-state)
18 search-subscriptions=(map query search-state)
14==
19==
15−+$ poke
16− $%
17− [%search query=tape]
18− ==
19++ rank-results
20++ rank-results
20 |= search-results=(map term (unit (list search-result)))
21 |= search-results=(map term (unit (list search-result)))
21 %- rank
22 %- rank
@@ -37,44 +38,123 @@ $:
37 |= [engine-name=term engine=engine *]
38 |= [engine-name=term engine=engine *]
38 =/ req
39 =/ req
39 ^- task:iris
40 ^- task:iris
40− [%request [%'GET' (crip (url.engine (trip query))) ~ ~] *outbound-config:iris]
41+ [%request (request.engine (trip query)) *outbound-config:iris]
41 [%pass ~[%search-result query engine-name] %arvo %i req]
42 [%pass ~[%search-result query engine-name] %arvo %i req]
43+:: +serve: the frontend, served from /web in this desk at /apps/seax/
44+::
45+++ serve
46+ |= [=bowl:gall url=@t]
47+ ^- simple-payload:http
48+ =/ [[ext=(unit @ta) site=(list @t)] *] (parse-request-line:server url)
49+ ?+ site not-found:gen:server
50+ [%apps %seax ~]
51+ (redirect:gen:server '/apps/seax/')
52+ ::
53+ [%apps %seax %$ ~]
54+ (serve-file bowl /index/html)
55+ ::
56+ [%apps %seax *]
57+ ?~ ext not-found:gen:server
58+ =/ pax=(list @t) (snoc t.t.site u.ext)
59+ ?. (levy pax (sane %ta)) not-found:gen:server
60+ (serve-file bowl ;;(path pax))
61+ ==
62+++ serve-file
63+ |= [=bowl:gall pax=path]
64+ ^- simple-payload:http
65+ =/ root=path /(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)
66+ =/ file=path (weld root (welp /web pax))
67+ ?. .^(? %cu file) not-found:gen:server
68+ =/ =mark ;;(mark (rear pax))
69+ =/ =dais:clay .^(dais:clay %cb (snoc root mark))
70+ =/ =tube:clay .^(tube:clay %cc (weld root /[mark]/mime))
71+ =/ =mime !<(mime (tube (vale:dais .^(* %cx file))))
72+ [[200 ['content-type' (rsh [3 1] (spat p.mime))]~] `q.mime]
42--
73--
43
74
44−=/ state *state-0
75+=/ state *state-3
76+=* current-engines
77+ |= =query
78+ ^- (list [term ?(%loading %failed %completed)])
79+ %+ turn ~(tap in ~(key by engines))
80+ |= engine=term
81+ :- engine
82+ =/ results
83+ (~(get by search-results:(~(got by search-subscriptions.state) query)) engine)
84+ ?~ results %loading
85+ ?~ +.results %failed
86+ ?~ +>.results %failed
87+ %completed
45=* search-results
88=* search-results
46 |= =query
89 |= =query
47 %- rank-results
90 %- rank-results
48 =/ search-state=search-state
91 =/ search-state=search-state
49 (~(got by search-subscriptions.state) query)
92 (~(got by search-subscriptions.state) query)
50 search-results.search-state
93 search-results.search-state
94+=* sink-for-query
95+ |= =query
96+ [(current-engines query) (search-results query)]
51=* sink
97=* sink
52 |= =query
98 |= =query
53− ((sink:^sink ~[%search query]) (search-results query))
99+ ((sink:^sink ~[~[%search query]]) (sink-for-query query))
54−:: =/ sinks *(map query _(sink ''))
55
100
56^- agent:gall
101^- agent:gall
57%- agent:dbug
102%- agent:dbug
58|_ =bowl:gall
103|_ =bowl:gall
59+* this .
104+* this .
105+ eyre-connect-card [%pass /eyre/connect %arvo %e %connect [~ /apps/seax] dap.bowl]
60−++ on-init `..on-init
106+++ on-init
107+ [~[eyre-connect-card] this]
61++ on-save !>(state)
108++ on-save !>(state)
62−++ on-load
109+++ on-load
63 |= =vase
110 |= =vase
64− =/ state !<(state-0 vase)
65− :- ~
66− %= this
67− state state
68− ==
69−++ on-poke
70− |= =cage
71 ^- (quip card:agent:gall _this)
111 ^- (quip card:agent:gall _this)
112+ ?: ?=([%3 *] q.vase)
113+ [~ this(state !<(state-3 vase))]
114+ :: an older state. its searches are dropped, but gall still has their
115+ :: subscribers, so kick them, or they would leave a new search for the
116+ :: same query. a state-0 agent stayed suspended until this upgrade, so
117+ :: its searches are stale anyway
118+ ::
119+ =/ stale=(list path)
120+ %~ tap in
121+ %- silt
122+ %+ murn ~(val by sup.bowl)
123+ |= [ship =path]
124+ ?.(?=([%search *] path) ~ `path)
125+ =/ kick=(list card:agent:gall)
126+ ?~(stale ~ [%give %kick stale ~]~)
127+ :: call off the pings to peers that are still waiting for an answer
128+ ::
129+ =/ leaves=(list card:agent:gall)
130+ %+ murn ~(tap by wex.bowl)
131+ |= [[=wire =ship =term] *]
132+ ^- (unit card:agent:gall)
133+ ?. ?=([%alive ~] wire) ~
134+ `[%pass wire %agent [ship term] %leave ~]
72− `this
135+ :_ this(state [%3 ~])
73−
136+ [eyre-connect-card (weld kick leaves)]
137+++ on-poke
138+ |= [=mark =vase]
139+ ^- (quip card:agent:gall _this)
140+ :: only eyre pokes us. older versions also took peers from other ships
141+ ::
142+ ?> ?=(%handle-http-request mark)
143+ =/ [eyre-id=@ta =inbound-request:eyre] !<([@ta inbound-request:eyre] vase)
144+ :_ this
145+ %+ give-simple-payload:app:server eyre-id
146+ %+ require-authorization:app:server inbound-request
147+ |= =inbound-request:eyre
148+ (serve bowl url.request.inbound-request)
74−++ on-watch
149+++ on-watch
75− |= =path
150+ |= =path
76 ?+ path !!
151 ?+ path !!
152+ [%http-response *]
153+ `this
77 [%search query ~]
154 [%search query ~]
155+ :: searching makes requests to every engine, so only we can do it
156+ ::
157+ ?> =(our src):bowl
78 =/ query +<.path
158 =/ query +<.path
79 =/ subscription=(unit search-state)
159 =/ subscription=(unit search-state)
80 (~(get by search-subscriptions.state) query)
160 (~(get by search-subscriptions.state) query)
@@ -82,7 +162,7 @@ $:
82 =. search-subscriptions.state
162 =. search-subscriptions.state
83 (~(put by search-subscriptions.state) query [1 ~])
163 (~(put by search-subscriptions.state) query [1 ~])
84 =/ cards (search-cards query)
164 =/ cards (search-cards query)
85− [cards this]
165+ [[flush:(sink query) cards] this]
86 =/ subscription=search-state +.subscription
166 =/ subscription=search-state +.subscription
87 =. subscription
167 =. subscription
88 [+(number-of-subscriptions.subscription) search-results.subscription]
168 [+(number-of-subscriptions.subscription) search-results.subscription]
@@ -91,35 +171,43 @@ $:
91 [~[flush:(sink query)] this]
171 [~[flush:(sink query)] this]
92 ==
172 ==
93
173
94−:: This was not tested because it requires waiting 12 hours.
95++ on-leave
174++ on-leave
96 |= =path
175 |= =path
97− ~& path
98− ?+ path !!
176+ ?+ path `this
99− [%search query ~]
177+ [%search query ~]
100 =/ query +<.path
178 =/ query +<.path
101 ~& path
179 ~& path
102− =/ search-state (~(got by search-subscriptions.state) query)
180+ =/ search-state (~(get by search-subscriptions.state) query)
181+ :: searches from before an upgrade are dropped in +on-load
182+ ::
183+ ?~ search-state `this
103− =/ number-of-subscriptions (dec number-of-subscriptions.search-state)
184+ =/ number-of-subscriptions (dec number-of-subscriptions.u.search-state)
104 =. search-subscriptions.state
185 =. search-subscriptions.state
105− ?~ number-of-subscriptions
186+ ?~ number-of-subscriptions
106 (~(del by search-subscriptions.state) query)
187 (~(del by search-subscriptions.state) query)
107− (~(put by search-subscriptions.state) query search-state(number-of-subscriptions number-of-subscriptions))
188+ (~(put by search-subscriptions.state) query u.search-state(number-of-subscriptions number-of-subscriptions))
108 `this
189 `this
109 ==
190 ==
110
191
111−++ on-peek
192+++ on-peek
112 |= path
193 |= path
113 ~
194 ~
195+:: older versions poked their peers with peer lists, and the acks to
196+:: those pokes can still arrive after an upgrade
197+::
198+++ on-agent
114−++ on-agent |=([wire sign:agent:gall] !!)
199+ |= [=wire =sign:agent:gall]
200+ ^- (quip card:agent:gall _this)
201+ `this
115++ on-arvo
202++ on-arvo
116− |= [=wire =sign-arvo]
203+ |= [=wire =sign-arvo]
117 ^- (quip card:agent:gall _this)
204 ^- (quip card:agent:gall _this)
118 ~& wire
205 ~& wire
119 ?+ wire `this
206 ?+ wire `this
120 [%search-result query=@t engine=term ~]
207 [%search-result query=@t engine=term ~]
121 ?> ?=([%iris %http-response %finished *] sign-arvo)
208 ?> ?=([%iris %http-response %finished *] sign-arvo)
122 =/ query=@t +<.wire
209 =/ query=@t +<.wire
210+ =/ sink (sink query)
123 =/ engine-name=term +>-.wire
211 =/ engine-name=term +>-.wire
124 =/ [=engine *] (~(got by engines) engine-name)
212 =/ [=engine *] (~(got by engines) engine-name)
125 =/ search-subscription (~(got by search-subscriptions.state) query)
213 =/ search-subscription (~(got by search-subscriptions.state) query)
@@ -130,9 +218,12 @@ $:
130 =. search-results.search-subscription
218 =. search-results.search-subscription
131 (~(put by search-results.search-subscription) engine-name results)
219 (~(put by search-results.search-subscription) engine-name results)
132 =. search-subscriptions.state
220 =. search-subscriptions.state
133− (~(put by search-subscriptions.state) query search-subscription)
221+ (~(put by search-subscriptions.state) query search-subscription)
222+ =^ card sink (sync:sink (sink-for-query query))
223+ ~& (current-engines query)
134− `this
224+ [~[card] this]
135 ==
225 ==
226+++ on-fail
136−++ on-fail |=([term tang] `..on-init)
227+ |= [term =tang]
228+ ((slog tang) `this)
137--
229--
138−
zod/seax/desk.docket-0new file+10
@@ -0,0 +1,10 @@
1+:~
2+ title+'%seax'
3+ info+'The Urbit meta-search engine. Sends your query to several search engines from your ship and merges their results.'
4+ color+0xff.ffff
5+ image+'/apps/seax/assets/img/tile.png'
6+ site+/apps/seax
7+ version+[0 1 0]
8+ website+'https://github.com/iko-soy/seax'
9+ license+'MIT'
10+==
zod/seax/lib/dbug.hoon+9−3
@@ -4,7 +4,8 @@
4::
4::
5|%
5|%
6+$ poke
6+$ poke
7+ $% [%skip depth=@ud =poke]
7− $% [%bowl ~]
8+ [%bowl ~]
8 [%state grab=cord]
9 [%state grab=cord]
9 [%incoming =about]
10 [%incoming =about]
10 [%outgoing =about]
11 [%outgoing =about]
@@ -32,8 +33,13 @@
32 ?. ?=(%dbug mark)
33 ?. ?=(%dbug mark)
33 =^ cards agent (on-poke:ag mark vase)
34 =^ cards agent (on-poke:ag mark vase)
34 [cards this]
35 [cards this]
35− =/ dbug
36− !<(poke vase)
36+ =/ dbug !<(poke vase)
37+ =? dbug ?=([%skip %0 *] dbug) poke.dbug
38+ ?: ?=(%skip -.dbug)
39+ =^ cards agent
40+ %+ on-poke:ag %dbug
41+ !>(`poke`dbug(depth (dec depth.dbug)))
42+ [cards this]
37 =; =tang
43 =; =tang
38 ((%*(. slog pri 1) tang) [~ this])
44 ((%*(. slog pri 1) tang) [~ this])
39 ?- -.dbug
45 ?- -.dbug
zod/seax/lib/docket.hoonnew file+223
@@ -0,0 +1,223 @@
1+/- *docket
2+|%
3+::
4+++ mime
5+ |%
6+ +$ draft
7+ $: title=(unit @t)
8+ info=(unit @t)
9+ color=(unit @ux)
10+ glob-http=(unit [=url hash=@uvH])
11+ glob-ames=(unit [=ship hash=@uvH])
12+ base=(unit term)
13+ site=(unit path)
14+ image=(unit url)
15+ version=(unit version)
16+ website=(unit url)
17+ license=(unit cord)
18+ ==
19+ ::
20+ ++ finalize
21+ |= =draft
22+ ^- (unit docket)
23+ ?~ title.draft ~
24+ ?~ info.draft ~
25+ ?~ color.draft ~
26+ ?~ version.draft ~
27+ ?~ website.draft ~
28+ ?~ license.draft ~
29+ =/ href=(unit href)
30+ ?^ site.draft `[%site u.site.draft]
31+ ?~ base.draft ~
32+ ?^ glob-http.draft
33+ `[%glob u.base hash.u.glob-http %http url.u.glob-http]:draft
34+ ?~ glob-ames.draft
35+ ~
36+ `[%glob u.base hash.u.glob-ames %ames ship.u.glob-ames]:draft
37+ ?~ href ~
38+ =, draft
39+ :- ~
40+ :* %1
41+ u.title
42+ u.info
43+ u.color
44+ u.href
45+ image
46+ u.version
47+ u.website
48+ u.license
49+ ==
50+ ::
51+ ++ from-clauses
52+ =| =draft
53+ |= cls=(list clause)
54+ ^- (unit docket)
55+ =* loop $
56+ ?~ cls (finalize draft)
57+ =* clause i.cls
58+ =. draft
59+ ?- -.clause
60+ %title draft(title `title.clause)
61+ %info draft(info `info.clause)
62+ %color draft(color `color.clause)
63+ %glob-http draft(glob-http `[url hash]:clause)
64+ %glob-ames draft(glob-ames `[ship hash]:clause)
65+ %base draft(base `base.clause)
66+ %site draft(site `path.clause)
67+ %image draft(image `url.clause)
68+ %version draft(version `version.clause)
69+ %website draft(website `website.clause)
70+ %license draft(license `license.clause)
71+ ==
72+ loop(cls t.cls)
73+ ::
74+ ++ to-clauses
75+ |= d=docket
76+ ^- (list clause)
77+ %- zing
78+ :~ :~ title+title.d
79+ info+info.d
80+ color+color.d
81+ version+version.d
82+ website+website.d
83+ license+license.d
84+ ==
85+ ?~ image.d ~ ~[image+u.image.d]
86+ ?: ?=(%site -.href.d) ~[site+path.href.d]
87+ =/ ref=glob-reference glob-reference.href.d
88+ :~ base+base.href.d
89+ ?- -.location.ref
90+ %http [%glob-http url.location.ref hash.ref]
91+ %ames [%glob-ames ship.location.ref hash.ref]
92+ == == ==
93+ ::
94+ ++ spit-clause
95+ |= =clause
96+ ^- tape
97+ %+ weld " {(trip -.clause)}+"
98+ ?+ -.clause "'{(trip +.clause)}'"
99+ %color (scow %ux color.clause)
100+ %site (spud path.clause)
101+ ::
102+ %glob-http
103+ "['{(trip url.clause)}' {(scow %uv hash.clause)}]"
104+ ::
105+ %glob-ames
106+ "[{(scow %p ship.clause)} {(scow %uv hash.clause)}]"
107+ ::
108+ %version
109+ =, version.clause
110+ "[{(scow %ud major)} {(scow %ud minor)} {(scow %ud patch)}]"
111+ ==
112+ ::
113+ ++ spit-docket
114+ |= dock=docket
115+ ^- tape
116+ ;: welp
117+ ":~\0a"
118+ `tape`(zing (join "\0a" (turn (to-clauses dock) spit-clause)))
119+ "\0a=="
120+ ==
121+ --
122+::
123+++ enjs
124+ =, enjs:format
125+ |%
126+ ::
127+ ++ charge-update
128+ |= u=^charge-update
129+ ^- json
130+ %+ frond -.u
131+ ^- json
132+ ?- -.u
133+ %del-charge s+desk.u
134+ ::
135+ %initial
136+ %- pairs
137+ %+ turn ~(tap by initial.u)
138+ |=([=desk c=^charge] [desk (charge c)])
139+ ::
140+ %add-charge
141+ %- pairs
142+ :~ desk+s+desk.u
143+ charge+(charge charge.u)
144+ ==
145+ ==
146+ ::
147+ ++ num
148+ |= a=@u
149+ ^- ^tape
150+ =/ p=json (numb a)
151+ ?> ?=(%n -.p)
152+ (trip p.p)
153+ ::
154+ ++ version
155+ |= v=^version
156+ ^- json
157+ :- %s
158+ %- crip
159+ "{(num major.v)}.{(num minor.v)}.{(num patch.v)}"
160+ ::
161+ ++ merge
162+ |= [a=json b=json]
163+ ^- json
164+ ?> &(?=(%o -.a) ?=(%o -.b))
165+ [%o (~(uni by p.a) p.b)]
166+ ::
167+ ++ href
168+ |= h=^href
169+ %+ frond -.h
170+ ?- -.h
171+ %site s+(spat path.h)
172+ %glob
173+ %- pairs
174+ :~ base+s+base.h
175+ glob-reference+(glob-reference glob-reference.h)
176+ ==
177+ ==
178+ ::
179+ ++ glob-reference
180+ |= ref=^glob-reference
181+ %- pairs
182+ :~ hash+s+(scot %uv hash.ref)
183+ location+(glob-location location.ref)
184+ ==
185+ ::
186+ ++ glob-location
187+ |= loc=^glob-location
188+ ^- json
189+ %+ frond -.loc
190+ ?- -.loc
191+ %http s+url.loc
192+ %ames s+(scot %p ship.loc)
193+ ==
194+ ::
195+ ++ charge
196+ |= c=^charge
197+ %+ merge (docket docket.c)
198+ %- pairs
199+ :~ chad+(chad chad.c)
200+ ==
201+ ::
202+ ++ docket
203+ |= d=^docket
204+ ^- json
205+ %- pairs
206+ :~ title+s+title.d
207+ info+s+info.d
208+ color+s+(scot %ux color.d)
209+ href+(href href.d)
210+ image+?~(image.d ~ s+u.image.d)
211+ version+(version version.d)
212+ license+s+license.d
213+ website+s+website.d
214+ ==
215+ ::
216+ ++ chad
217+ |= c=^chad
218+ %+ frond -.c
219+ ?+ -.c ~
220+ %hung s+err.c
221+ ==
222+ --
223+--
zod/seax/lib/engines.hoon+2−3
@@ -1,14 +1,13 @@
1/- *seax
1/- *seax
2−/+ engines-yep, engines-crowdview, engines-wiby, engines-alexandria
2+/+ engines-crowdview, engines-wiby, engines-urbit-dev
3
3
4^- engines
4^- engines
5
5
6%- malt
6%- malt
7%- limo
7%- limo
8:~
8:~
9− [name=%yep engine=engines-yep weight=.1]
10 [name=%crowdview engine=engines-crowdview weight=.2]
9 [name=%crowdview engine=engines-crowdview weight=.2]
11 [name=%wiby engine=engines-wiby weight=.2]
10 [name=%wiby engine=engines-wiby weight=.2]
12− [name=%alexandria engine=engines-alexandria weight=.2]
11+ [name=%urbit-dev engine=engines-urbit-dev weight=.2]
13==
12==
14
13
zod/seax/lib/engines/alexandria.hoondeleted−32
@@ -1,32 +0,0 @@
1−/- *seax
2−
3−^- engine
4−=<
5−|%
6−++ url
7− |= query=tape
8− ^- tape
9− %+ weld
10− "https://api.alexandria.org/?a=1&q="
11− (en-urlt:html query)
12−++ results
13− |= response=mime-data:iris
14− ^- (unit (list search-result))
15− =/ body q.data.response
16− ;< jon=json _biff (de:json:html body)
17− (apex jon)
18−--
19−::
20−|%
21−++ apex
22− =, dejs-soft:format
23− %- ot
24− ~[results+(ar parse-search-result)]
25−++ parse-search-result
26− =, dejs-soft:format
27− %- ot
28− ~[title+so url+so]
29−--
30−
31−
32−
zod/seax/lib/engines/crowdview.hoon+3−5
@@ -3,12 +3,10 @@
3^- engine
3^- engine
4=<
4=<
5|%
5|%
6−++ url
6+++ request
7 |= query=tape
7 |= query=tape
8− ^- tape
8+ ^- request:http
9− %+ weld
10− "https://crowdview-next-js.onrender.com/api/search-v3?query="
9+ (get "https://crowdview-next-js.onrender.com/api/search-v3?query=" query)
11− (en-urlt:html query)
12++ results
10++ results
13 |= response=mime-data:iris
11 |= response=mime-data:iris
14 ^- (unit (list search-result))
12 ^- (unit (list search-result))
zod/seax/lib/engines/urbit-dev.hoonnew file+36
@@ -0,0 +1,36 @@
1+/- *seax
2+
3+^- engine
4+=<
5+|%
6+++ request
7+ |= query=tape
8+ ^- request:http
9+ :^ %'POST' 'https://docs.urbit.org/~gitbook/search'
10+ ~[['content-type' 'application/json']]
11+ %- some
12+ %- as-octs:mimes:html
13+ %- en:json:html
14+ %- pairs:enjs:format
15+ :~ query+s+(crip query)
16+ scope+(pairs:enjs:format ~[mode+s+'all'])
17+ ==
18+++ results
19+ |= response=mime-data:iris
20+ ^- (unit (list search-result))
21+ =/ body q.data.response
22+ ;< jon=json _biff (de:json:html body)
23+ (apex jon)
24+--
25+::
26+|%
27+++ apex
28+ =, dejs-soft:format
29+ (ar parse-search-result)
30+++ parse-search-result
31+ |= jon=json
32+ ^- (unit search-result)
33+ =, dejs-soft:format
34+ ;< [title=@t href=@t] _biff ((ot ~[title+so href+so]) jon)
35+ `[title (cat 3 'https://docs.urbit.org' href)]
36+--
zod/seax/lib/engines/wiby.hoon+3−5
@@ -3,12 +3,10 @@
3^- engine
3^- engine
4=<
4=<
5|%
5|%
6−++ url
6+++ request
7 |= query=tape
7 |= query=tape
8− ^- tape
8+ ^- request:http
9− %+ weld
10− "https://wiby.me/json/?q="
9+ (get "https://wiby.me/json/?q=" query)
11− (en-urlt:html query)
12++ results
10++ results
13 |= response=mime-data:iris
11 |= response=mime-data:iris
14 ^- (unit (list search-result))
12 ^- (unit (list search-result))
zod/seax/lib/engines/yep.hoondeleted−30
@@ -1,30 +0,0 @@
1−/- *seax
2−
3−^- engine
4−=<
5−|%
6−++ url
7− |= query=tape
8− ^- tape
9− %+ weld
10− "https://api.yep.com/fs/1/?type=web&no_correct=false&limit=100&q="
11− (en-urlt:html query)
12−++ results
13− |= response=mime-data:iris
14− =/ body q.data.response
15− ;< jon=json _biff (de:json:html body)
16− (apex jon)
17−--
18−::
19−|%
20−++ apex
21− |= jon=json
22− ^- (unit (list search-result))
23− =, dejs-soft:format
24− ;< [* results=(list search-result)] _biff %- (at ~[so (ot ~[results+(ar parse-search-result)])]) jon
25− `results
26−++ parse-search-result
27− =, dejs-soft:format
28− %- ot
29− ~[title+so url+so]
30−--
zod/seax/lib/rank.hoon+10−11
@@ -3,15 +3,15 @@
3
3
4=<
4=<
5|= results=(list [engine=term results=(list search-result)])
5|= results=(list [engine=term results=(list search-result)])
6−^- (list search-result)
6+^- (list [engines=(list term) =search-result])
7=/ url-to-engine-map
7=/ url-to-engine-map
8 ^- (list (map @t [engines=(list term) ords=(list @u) result=search-result]))
8 ^- (list (map @t [engines=(list term) ords=(list @u) result=search-result]))
9 %- zing
9 %- zing
10 %+ turn results
10 %+ turn results
11 |= [engine=term results=(list search-result)]
11 |= [engine=term results=(list search-result)]
12− %+ turn (zip (gulf 1 (lent results)) results)
12+ %+ turn (zip (gulf 0 (lent results)) results)
13 |= [ord=@u result=search-result]
13 |= [ord=@u result=search-result]
14− %- malt ~[[url.result [~[engine] ~[ord] result]]]
14+ %- malt ~[[url.result [~[engine] ~[+(ord)] result]]]
15=/ url-to-engines-map
15=/ url-to-engines-map
16 ^- (map @t [engines=(list term) ords=(list @u) result=search-result])
16 ^- (map @t [engines=(list term) ords=(list @u) result=search-result])
17 %+ roll url-to-engine-map
17 %+ roll url-to-engine-map
@@ -29,7 +29,7 @@
29 ==
29 ==
30 [(weld engines-lhs engines-rhs) (weld ords-lhs ords-rhs) result]
30 [(weld engines-lhs engines-rhs) (weld ords-lhs ords-rhs) result]
31=/ ranking
31=/ ranking
32− ^- (list [ord=@rs result=search-result])
32+ ^- (list [ord=@rs result=[engines=(list term) =search-result]])
33 %+ turn ~(val by url-to-engines-map)
33 %+ turn ~(val by url-to-engines-map)
34 |= [engines=(list term) ords=(list @u) result=search-result]
34 |= [engines=(list term) ords=(list @u) result=search-result]
35 =/ engine-weights
35 =/ engine-weights
@@ -42,24 +42,23 @@
42 ^- @rs
42 ^- @rs
43 %+ roll engine-weights
43 %+ roll engine-weights
44 |= [a=_.1 b=_.1]
44 |= [a=_.1 b=_.1]
45− (mul.rs a b)
45+ (mul:rs a b)
46 =/ position-weights
46 =/ position-weights
47 ^- @rs
47 ^- @rs
48− %+ mul.rs (lent ords)
48+ %+ mul:rs (sun:rs (lent ords))
49 %+ roll
49 %+ roll
50− (turn ords |=(ord=@u (div.rs 1 (sun:rs ord))))
50+ (turn ords |=(ord=@u (div:rs .1 (sun:rs ord))))
51 add
51 add
52 =/ rank
52 =/ rank
53 ^- @rs
53 ^- @rs
54− (mul.rs engine-weights position-weights)
54+ (mul:rs engine-weights position-weights)
55− [ord=rank result=result]
55+ [ord=rank result=[engines result]]
56=/ sorted-results
56=/ sorted-results
57 %+ sort ranking
57 %+ sort ranking
58 |= [[lhs=@rs *] [rhs=@rs *]]
58 |= [[lhs=@rs *] [rhs=@rs *]]
59 %+ gth lhs rhs
59 %+ gth lhs rhs
60−~& sorted-results
61%+ turn sorted-results
60%+ turn sorted-results
62−|= [* result=search-result]
61+|= [* result=[(list term) search-result]]
63result
62result
64
63
65|%
64|%
zod/seax/lib/server.hoonnew file+159
@@ -0,0 +1,159 @@
1+=, eyre
2+|%
3++$ request-line
4+ $: [ext=(unit @ta) site=(list @t)]
5+ args=(list [key=@t value=@t])
6+ ==
7+:: +parse-request-line: take a cord and parse out a url
8+::
9+++ parse-request-line
10+ |= url=@t
11+ ^- request-line
12+ (fall (rush url ;~(plug apat:de-purl:html yque:de-purl:html)) [[~ ~] ~])
13+::
14+++ manx-to-octs
15+ |= man=manx
16+ ^- octs
17+ (as-octt:mimes:html (en-xml:html man))
18+::
19+++ json-to-octs
20+ |= jon=json
21+ ^- octs
22+ (as-octs:mimes:html (en:json:html jon))
23+::
24+++ app
25+ |%
26+ ::
27+ :: +require-authorization:
28+ :: redirect to the login page when unauthenticated
29+ :: otherwise call handler on inbound request
30+ ::
31+ ++ require-authorization
32+ |= $: =inbound-request:eyre
33+ handler=$-(inbound-request:eyre simple-payload:http)
34+ ==
35+ ^- simple-payload:http
36+ ::
37+ ?: authenticated.inbound-request
38+ ~! this
39+ ~! +:*handler
40+ (handler inbound-request)
41+ ::
42+ =- [[307 ['location' -]~] ~]
43+ %^ cat 3
44+ '/~/login?redirect='
45+ url.request.inbound-request
46+ ::
47+ :: +require-authorization-simple:
48+ :: redirect to the login page when unauthenticated
49+ :: otherwise pass through simple-paylod
50+ ::
51+ ++ require-authorization-simple
52+ |= [=inbound-request:eyre =simple-payload:http]
53+ ^- simple-payload:http
54+ ::
55+ ?: authenticated.inbound-request
56+ ~! this
57+ simple-payload
58+ ::
59+ =- [[307 ['location' -]~] ~]
60+ %^ cat 3
61+ '/~/login?redirect='
62+ url.request.inbound-request
63+ ::
64+ ++ give-simple-payload
65+ |= [eyre-id=@ta =simple-payload:http]
66+ ^- (list card:agent:gall)
67+ =/ header-cage
68+ [%http-response-header !>(response-header.simple-payload)]
69+ =/ data-cage
70+ [%http-response-data !>(data.simple-payload)]
71+ :~ [%give %fact ~[/http-response/[eyre-id]] header-cage]
72+ [%give %fact ~[/http-response/[eyre-id]] data-cage]
73+ [%give %kick ~[/http-response/[eyre-id]] ~]
74+ ==
75+ --
76+++ gen
77+ |%
78+ ::
79+ ++ max-1-da ['cache-control' 'max-age=86400']
80+ ++ max-1-wk ['cache-control' 'max-age=604800']
81+ ::
82+ ++ html-response
83+ =| cache=?
84+ |= =octs
85+ ^- simple-payload:http
86+ :_ `octs
87+ [200 [['content-type' 'text/html'] ?:(cache [max-1-wk ~] ~)]]
88+ ::
89+ ++ css-response
90+ =| cache=?
91+ |= =octs
92+ ^- simple-payload:http
93+ :_ `octs
94+ [200 [['content-type' 'text/css'] ?:(cache [max-1-wk ~] ~)]]
95+ ::
96+ ++ js-response
97+ =| cache=?
98+ |= =octs
99+ ^- simple-payload:http
100+ :_ `octs
101+ [200 [['content-type' 'text/javascript'] ?:(cache [max-1-wk ~] ~)]]
102+ ::
103+ ++ png-response
104+ =| cache=?
105+ |= =octs
106+ ^- simple-payload:http
107+ :_ `octs
108+ [200 [['content-type' 'image/png'] ?:(cache [max-1-wk ~] ~)]]
109+ ::
110+ ++ svg-response
111+ =| cache=?
112+ |= =octs
113+ ^- simple-payload:http
114+ :_ `octs
115+ [200 [['content-type' 'image/svg+xml'] ?:(cache [max-1-wk ~] ~)]]
116+ ::
117+ ++ ico-response
118+ |= =octs
119+ ^- simple-payload:http
120+ [[200 [['content-type' 'image/x-icon'] max-1-wk ~]] `octs]
121+ ::
122+ ++ woff2-response
123+ =| cache=?
124+ |= =octs
125+ ^- simple-payload:http
126+ [[200 [['content-type' 'font/woff2'] max-1-wk ~]] `octs]
127+ ::
128+ ++ json-response
129+ =| cache=_|
130+ |= =json
131+ ^- simple-payload:http
132+ :_ `(json-to-octs json)
133+ [200 [['content-type' 'application/json'] ?:(cache [max-1-da ~] ~)]]
134+ ::
135+ ++ manx-response
136+ =| cache=_|
137+ |= man=manx
138+ ^- simple-payload:http
139+ :_ `(manx-to-octs man)
140+ [200 [['content-type' 'text/html'] ?:(cache [max-1-da ~] ~)]]
141+ ::
142+ ++ not-found
143+ ^- simple-payload:http
144+ [[404 ~] ~]
145+ ::
146+ ++ login-redirect
147+ |= =request:http
148+ ^- simple-payload:http
149+ =- [[307 ['location' -]~] ~]
150+ %^ cat 3
151+ '/~/login?redirect='
152+ url.request
153+ ::
154+ ++ redirect
155+ |= redirect=cord
156+ ^- simple-payload:http
157+ [[307 ['location' redirect]~] ~]
158+ --
159+--
zod/seax/lib/sink.hoon+1−1
@@ -21,6 +21,6 @@
21 ++ paths pats
21 ++ paths pats
22 ++ flush
22 ++ flush
23 ^- card:agent:gall
23 ^- card:agent:gall
24− [%give %fact pats %noun !>(^-((clog) [%flush stat]))]
24+ [%give %fact ~ %noun !>(^-((clog) [%flush stat]))]
25 --
25 --
26--
26--
zod/seax/mar/css.hoonnew file+25
@@ -0,0 +1,25 @@
1+::
2+:::: /hoon/css/mar
3+ ::
4+/? 310
5+=, eyre
6+=, mimes:html
7+|_ mud=@t
8+++ grow :: convert to
9+ |%
10+ ++ mime [/text/css (as-octs mud)] :: convert to %mime
11+ ++ hymn :: convert to %hymn
12+ |^ html
13+ ++ style ;style
14+ ;- (trip mud)
15+ ==
16+ ++ html ;html:(head:"{style}" body)
17+ --
18+ --
19+++ grab
20+ |% :: convert from
21+ ++ mime |=([p=mite q=octs] (@t q.q))
22+ ++ noun @t :: clam from %noun
23+ --
24+++ grad %mime
25+--
zod/seax/mar/docket-0.hoonnew file+25
@@ -0,0 +1,25 @@
1+/+ dock=docket
2+|_ =docket:dock
3+++ grow
4+ |%
5+ ++ mime
6+ ^- ^mime
7+ [/text/x-docket (as-octt:mimes:html (spit-docket:mime:dock docket))]
8+ ++ noun docket
9+ ++ json (docket:enjs:dock docket)
10+ --
11+++ grab
12+ |%
13+ ::
14+ ++ mime
15+ |= [=mite len=@ud tex=@]
16+ ^- docket:dock
17+ %- need
18+ %- from-clauses:mime:dock
19+ !<((list clause:dock) (slap !>(~) (ream tex)))
20+
21+ ::
22+ ++ noun docket:dock
23+ --
24+++ grad %noun
25+--
zod/seax/mar/hoon.hoon+285−31
@@ -1,41 +1,295 @@
1−:::: /hoon/hoon/mar
2− ::
3−/? 310
4−::
5−=, eyre
6|_ own=@t
1|_ own=@t
7::
2::
8++ grow :: convert to
3++ grow :: convert to
9 |%
4 |%
10 ++ mime `^mime`[/text/x-hoon (as-octs:mimes:html own)] :: convert to %mime
5 ++ mime `^mime`[/text/x-hoon (as-octs:mimes:html own)] :: convert to %mime
11− ++ elem :: convert to %html
12− ;div:pre(urb_codemirror "", mode "hoon"):"{(trip own)}"
13− :: =+ gen-id="src-{<`@ui`(mug own)>}"
14− :: ;div
15− :: ;textarea(id "{gen-id}"):"{(trip own)}"
16− :: ;script:"""
17− :: CodeMirror.fromTextArea(
18− :: window[{<gen-id>}],
19− :: \{lineNumbers:true, readOnly:true}
20− :: )
21− :: """
22− :: ==
23 ++ hymn
6 ++ hymn
24− :: ;html:(head:title:"Source" "+{elem}")
7+ ^- manx
25− ;html
8+ =/ to-wall
26− ;head
9+ |= =tape
27− ;title:"Source"
10+ ^- wall
28− ;script@"//cdnjs.cloudflare.com/ajax/libs/codemirror/4.3.0/codemirror.js";
11+ %+ roll (flop tape)
29− ;script@"/lib/syntax/hoon.js";
12+ |= [char=@tD =wall]
30− ;link(rel "stylesheet", href "//cdnjs.cloudflare.com/ajax/libs/".
13+ ?~ wall
31− "codemirror/4.3.0/codemirror.min.css");
14+ [[char ~] ~]
32− ;link/"/lib/syntax/codemirror.css"(rel "stylesheet");
15+ ?: =('\0a' char)
33− ==
16+ [~ wall]
34− ;body
17+ [[char i.wall] t.wall]
18+ ::
35− ;textarea#src:"{(trip own)}"
19+ =/ src=tape (trip own)
36− ;script:'CodeMirror.fromTextArea(src, {lineNumbers:true, readOnly:true})'
20+ =/ count=@ud (lent (to-wall src))
21+ =; [style=tape script=tape]
22+ ;html
23+ ;head
24+ ;title: source
25+ ;meta(charset "utf-8");
26+ ;meta(name "viewport", content "width=device-width, initial-scale=1");
27+ ;style: {style}
28+ ==
29+ ;body
30+ ;div.viewer
31+ :: ;div.viewer-bar
32+ :: ;span.viewer-name: {(scow %ud count)} lines
33+ :: ;button.copy-link(id "copy-link", hidden ""): copy link
34+ :: ==
35+ ;div.code
36+ ;pre.gutter(id "gutter");
37+ ;div.code-body
38+ ;div(id "hl");
39+ ;pre.code-pre(id "code"): {src}
40+ ==
41+ ==
42+ ==
43+ ;script: {script}
44+ ==
37 ==
45 ==
38− ==
46+ ::
47+ :-
48+ ^~ ^= style
49+ ^- tape
50+ %- trip
51+ '''
52+ :root {
53+ --bg: #fbfbf9;
54+ --panel: #ffffff;
55+ --gutter-bg: #f3f3ef;
56+ --line: #e4e4dd;
57+ --fg: #2b2b2b;
58+ --fg-dim: #9a9a90;
59+ --sel: #fff7d6;
60+ --sel-edge: #f1e4a8;
61+ --accent: #4a6da7;
62+ --lh: 20px;
63+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
64+ "Liberation Mono", "Courier New", monospace;
65+ }
66+ * { box-sizing: border-box; }
67+ html, body {
68+ margin: 0;
69+ height: 100%;
70+ background: var(--bg);
71+ color: var(--fg);
72+ font-family: var(--mono);
73+ -webkit-font-smoothing: antialiased;
74+ }
75+ .viewer {
76+ height: 100vh;
77+ display: flex;
78+ flex-direction: column;
79+ padding: 16px;
80+ gap: 12px;
81+ }
82+ .viewer-bar {
83+ flex: 0 0 auto;
84+ display: flex;
85+ align-items: center;
86+ gap: 12px;
87+ font-size: 13px;
88+ color: var(--fg-dim);
89+ }
90+ .viewer-name {
91+ flex: 1 1 auto;
92+ min-width: 0;
93+ overflow: hidden;
94+ text-overflow: ellipsis;
95+ white-space: nowrap;
96+ color: var(--fg);
97+ }
98+ .copy-link {
99+ flex: 0 0 auto;
100+ padding: 4px 12px;
101+ border: 1px solid var(--line);
102+ border-radius: 6px;
103+ background: var(--panel);
104+ color: var(--fg-dim);
105+ font: inherit;
106+ font-size: 12px;
107+ cursor: pointer;
108+ transition: color .12s, border-color .12s;
109+ }
110+ .copy-link:hover { color: var(--accent); border-color: var(--accent); }
111+ .copy-link[hidden] { display: none; }
112+ .code {
113+ flex: 1 1 auto;
114+ min-height: 0;
115+ display: flex;
116+ align-items: flex-start;
117+ overflow: auto;
118+ background: var(--panel);
119+ border: 1px solid var(--line);
120+ border-radius: 8px;
121+ }
122+ .code-body {
123+ position: relative;
124+ flex: 1 1 auto;
125+ min-width: 0;
126+ width: max-content;
127+ }
128+ #gutter, #code {
129+ margin: 0;
130+ padding: 14px 0;
131+ font-family: var(--mono);
132+ font-size: 13px;
133+ line-height: var(--lh);
134+ white-space: pre;
135+ tab-size: 2;
136+ }
137+ #gutter {
138+ position: sticky;
139+ left: 0;
140+ z-index: 2;
141+ flex: 0 0 auto;
142+ padding-left: 16px;
143+ padding-right: 12px;
144+ text-align: right;
145+ color: var(--fg-dim);
146+ background: var(--gutter-bg);
147+ border-right: 1px solid var(--line);
148+ user-select: none;
149+ -webkit-user-select: none;
150+ cursor: pointer;
151+ }
152+ #code {
153+ display: block;
154+ padding-left: 14px;
155+ padding-right: 18px;
156+ color: var(--fg);
157+ }
158+ #hl {
159+ position: absolute;
160+ left: 0;
161+ right: 0;
162+ z-index: 0; /* under the text */
163+ display: none;
164+ background: var(--sel);
165+ box-shadow: inset 2px 0 0 var(--sel-edge);
166+ pointer-events: none;
167+ }
168+ #code { position: relative; z-index: 1; } /* text above the bar */
169+ :focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
170+ '''
171+ ^~ ^= code
172+ ^- tape
173+ %- trip
174+ '''
175+ (function () {
176+ "use strict";
177+ const panel = document.querySelector(".code"); // the scroll container
178+ const code = document.getElementById("code");
179+ const gutter = document.getElementById("gutter");
180+ const hl = document.getElementById("hl");
181+ if (!panel || !code || !gutter || !hl) return;
182+ const text = code.textContent;
183+ let lineCount = text.length ? text.split("\n").length : 1;
184+ if (text.endsWith("\n")) lineCount -= 1;
185+ if (lineCount < 1) lineCount = 1;
186+ (function fillGutter() {
187+ let s = "";
188+ for (let i = 1; i <= lineCount; i++) s += i + "\n";
189+ gutter.textContent = s;
190+ })();
191+ let lineH = 0;
192+ let codeTop = 0; // code's content-box top padding, the y-origin of line 1
193+ function measure() {
194+ const cs = getComputedStyle(code);
195+ const lh = parseFloat(cs.lineHeight);
196+ lineH = isNaN(lh) ? parseFloat(cs.fontSize) * 1.55 : lh;
197+ codeTop = parseFloat(cs.paddingTop) || 0;
198+ positionHighlight(); // keep the bar correct after a re-measure
199+ }
200+ let anchor = null;
201+ let selLo = 0;
202+ let selHi = 0;
203+ function positionHighlight() {
204+ if (!selLo) { hl.style.display = "none"; return; }
205+ hl.style.display = "block";
206+ hl.style.top = (codeTop + (selLo - 1) * lineH) + "px";
207+ hl.style.height = ((selHi - selLo + 1) * lineH) + "px";
208+ }
209+ function applySel(lo, hi) {
210+ selLo = lo;
211+ selHi = hi;
212+ positionHighlight();
213+ }
214+ function deselect() {
215+ selLo = selHi = 0;
216+ anchor = null;
217+ positionHighlight();
218+ history.replaceState(null, "", location.pathname + location.search);
219+ }
220+ function parseHash(hash) {
221+ const m = /^#L(\d+)(?:-L(\d+))?$/.exec(hash || "");
222+ if (!m) return null;
223+ let lo = parseInt(m[1], 10);
224+ let hi = m[2] ? parseInt(m[2], 10) : lo;
225+ if (hi < lo) { const t = lo; lo = hi; hi = t; }
226+ lo = Math.max(1, Math.min(lo, lineCount));
227+ hi = Math.max(1, Math.min(hi, lineCount));
228+ return [lo, hi];
229+ }
230+ function setHash(lo, hi) {
231+ const frag = lo === hi ? "L" + lo : "L" + lo + "-L" + hi;
232+ history.replaceState(null, "", "#" + frag);
233+ }
234+ function scrollToLine(n) {
235+ const y = codeTop + (n - 1) * lineH;
236+ if (y < panel.scrollTop || y > panel.scrollTop + panel.clientHeight) {
237+ panel.scrollTop = Math.max(0, y - panel.clientHeight / 2);
238+ }
239+ }
240+ function selectFromHash(scroll) {
241+ const r = parseHash(location.hash);
242+ if (!r) return false;
243+ anchor = r[0];
244+ applySel(r[0], r[1]);
245+ if (scroll) scrollToLine(r[0]);
246+ return true;
247+ }
248+ function lineFromEvent(e) {
249+ // y within the code content: pointer relative to the code pre's rendered
250+ // top, which already accounts for scroll since getBoundingClientRect is
251+ // viewport-relative and the pre moves as the panel scrolls.
252+ const rect = code.getBoundingClientRect();
253+ const y = e.clientY - rect.top - codeTop;
254+ let n = Math.floor(y / lineH) + 1;
255+ if (n < 1) n = 1;
256+ if (n > lineCount) n = lineCount;
257+ return n;
258+ }
259+ function onClick(e) {
260+ const inGutter = e.target === gutter;
261+ if (!inGutter && !e.shiftKey) return; // normal text interaction in code
262+ const n = lineFromEvent(e);
263+ if (!e.shiftKey && selLo === n && selHi === n) {
264+ deselect();
265+ return;
266+ }
267+ if (e.shiftKey && anchor !== null) {
268+ applySel(Math.min(anchor, n), Math.max(anchor, n));
269+ setHash(selLo, selHi);
270+ } else {
271+ anchor = n;
272+ applySel(n, n);
273+ setHash(n, n);
274+ }
275+ }
276+ gutter.addEventListener("click", onClick);
277+ code.addEventListener("click", onClick);
278+ document.addEventListener("keydown", function (e) {
279+ if (e.key === "Escape") deselect();
280+ });
281+ window.addEventListener("hashchange", function () {
282+ if (!selectFromHash(true)) deselect();
283+ });
284+ let rz = null;
285+ window.addEventListener("resize", function () {
286+ clearTimeout(rz);
287+ rz = setTimeout(measure, 100);
288+ });
289+ measure();
290+ selectFromHash(true);
291+ })();
292+ '''
39 ++ txt
293 ++ txt
40 (to-wain:format own)
294 (to-wain:format own)
41 --
295 --
zod/seax/mar/html.hoonnew file+22
@@ -0,0 +1,22 @@
1+::
2+:::: /hoon/html/mar
3+ ::
4+/? 310
5+ ::
6+:::: compute
7+ ::
8+=, html
9+|_ htm=@t
10+++ grow :: convert to
11+ ^?
12+ |% ::
13+ ++ mime [/text/html (met 3 htm) htm] :: to %mime
14+ ++ hymn (need (de-xml htm)) :: to %hymn
15+ -- ::
16+++ grab ^?
17+ |% :: convert from
18+ ++ noun @t :: clam from %noun
19+ ++ mime |=([p=mite q=octs] q.q) :: retrieve form %mime
20+ --
21+++ grad %mime
22+--
zod/seax/mar/js.hoonnew file+25
@@ -0,0 +1,25 @@
1+::
2+:::: /hoon/js/mar
3+ ::
4+/? 310
5+::
6+=, eyre
7+|_ mud=@
8+++ grow
9+ |%
10+ ++ mime [/application/javascript (as-octs:mimes:html (@t mud))]
11+ ++ hymn :: convert to %hymn
12+ |^ html
13+ ++ script ;script
14+ ;- (trip (@t mud))
15+ ==
16+ ++ html ;html:(head:"{script}" body)
17+ --
18+ --
19+++ grab
20+ |% :: convert from
21+ ++ mime |=([p=mite q=octs] (@t q.q))
22+ ++ noun cord :: clam from %noun
23+ --
24+++ grad %mime
25+--
zod/seax/mar/png.hoonnew file+12
@@ -0,0 +1,12 @@
1+|_ dat=@
2+++ grow
3+ |%
4+ ++ mime [/image/png (as-octs:mimes:html dat)]
5+ --
6+++ grab
7+ |%
8+ ++ mime |=([p=mite q=octs] q.q)
9+ ++ noun @
10+ --
11+++ grad %mime
12+--
zod/seax/mar/txt-diff.hoonnew file+16
@@ -0,0 +1,16 @@
1+::
2+:::: /hoon/txt-diff/mar
3+ ::
4+/? 310
5+|_ txt-diff=(urge:clay cord)
6+::
7+++ grad %noun
8+++ grow
9+ |%
10+ ++ noun txt-diff
11+ --
12+++ grab :: convert from
13+ |%
14+ ++ noun (urge:clay cord) :: make from %noun
15+ --
16+--
zod/seax/mar/txt.hoon−1
@@ -18,7 +18,6 @@
18 => v=.
18 => v=.
19 |%
19 |%
20 ++ mime => v [/text/plain (as-octs (of-wain txt))]
20 ++ mime => v [/text/plain (as-octs (of-wain txt))]
21− ++ elem => v ;pre: {(trip (of-wain txt))}
22 --
21 --
23++ grad
22++ grad
24 |%
23 |%
zod/seax/mar/woff2.hoonnew file+12
@@ -0,0 +1,12 @@
1+|_ dat=octs
2+++ grow
3+ |%
4+ ++ mime [/font/woff2 dat]
5+ --
6+++ grab
7+ |%
8+ ++ mime |=([=mite =octs] octs)
9+ ++ noun octs
10+ --
11+++ grad %mime
12+--
zod/seax/sur/docket.hoonnew file+82
@@ -0,0 +1,82 @@
1+|%
2+::
3++$ version
4+ [major=@ud minor=@ud patch=@ud]
5+::
6++$ glob (map path mime)
7+::
8++$ url cord
9+:: $glob-location: How to retrieve a glob
10+::
11++$ glob-reference
12+ [hash=@uvH location=glob-location]
13+::
14++$ glob-location
15+ $% [%http =url]
16+ [%ames =ship]
17+ ==
18+:: $href: Where a tile links to
19+::
20++$ href
21+ $% [%glob base=term =glob-reference]
22+ [%site =path]
23+ ==
24+:: $chad: State of a docket
25+::
26++$ chad
27+ $~ [%install ~]
28+ $% :: Done
29+ [%glob =glob]
30+ [%site ~]
31+ :: Waiting
32+ [%install ~]
33+ [%suspend glob=(unit glob)]
34+ :: Error
35+ [%hung err=cord]
36+ ==
37+::
38+:: $charge: A realized $docket
39+::
40++$ charge
41+ $: =docket
42+ =chad
43+ ==
44+::
45+:: $clause: A key and value, as part of a docket
46+::
47+:: Only used to parse $docket
48+::
49++$ clause
50+ $% [%title title=@t]
51+ [%info info=@t]
52+ [%color color=@ux]
53+ [%glob-http url=cord hash=@uvH]
54+ [%glob-ames =ship hash=@uvH]
55+ [%image =url]
56+ [%site =path]
57+ [%base base=term]
58+ [%version =version]
59+ [%website website=url]
60+ [%license license=cord]
61+ ==
62+::
63+:: $docket: A description of JS bundles for a desk
64+::
65++$ docket
66+ $: %1
67+ title=@t
68+ info=@t
69+ color=@ux
70+ =href
71+ image=(unit url)
72+ =version
73+ website=url
74+ license=cord
75+ ==
76+::
77++$ charge-update
78+ $% [%initial initial=(map desk charge)]
79+ [%add-charge =desk =charge]
80+ [%del-charge =desk]
81+ ==
82+--
zod/seax/sur/seax.hoon+10−1
@@ -7,8 +7,17 @@
7++ engine
7++ engine
8 $_ ^|
8 $_ ^|
9 |%
9 |%
10− ++ url |~ tape *tape
10+ ++ request |~ tape *request:http
11 ++ results |~ mime-data:iris *(unit (list search-result))
11 ++ results |~ mime-data:iris *(unit (list search-result))
12 --
12 --
13+$ engines (map name=term [=engine weight=@rs])
13+$ engines (map name=term [=engine weight=@rs])
14+:: +get: a GET request to url with the query appended
15+::
16+:: The runtime unescapes the query string once before sending the
17+:: request, so the query is escaped twice to stay escaped on the wire.
18+::
19+++ get
20+ |= [url=tape query=tape]
21+ ^- request:http
22+ [%'GET' (crip (weld url (en-urlt:html (en-urlt:html query)))) ~ ~]
14--
23--
zod/seax/sys.kelvin+1−1
@@ -1 +1 @@
1−[%zuse 413]
1+[%zuse 408]